From 7ff4b77fb9aa5fdefe724c7df3cd5dfe4bd5c967 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 13 Jul 2023 12:11:11 -0700 Subject: [PATCH] UI: Merge scheduling functionality into queries page (#12713) ## Addresses #12636 ### See issue for list work done ![Screenshot 2023-07-12 at 6 47 04 PM](https://github.com/fleetdm/fleet/assets/61553566/47e3e5b2-0195-4f54-a377-8e5c03313acf) ![Frame-12-07-2023-06-43-32](https://github.com/fleetdm/fleet/assets/61553566/f72f2d41-609f-4409-8595-5f3e4f06d9bb) ### Notes for review: - Because other work is based on this branch, TODOs / fixes are noted here until the team comes to a strategy for merging all of the work: - Add missing space in the Performance impact column "Undetermined" tooltip text - I'm having trouble confirming that the inherited queries table is working right with the mock hard-coded data, though I did see it working correctly previously. There's an issue with the page reverting to "All teams" when trying to show the inherited table, though it does show the table before re-rendering. - This work is organized clearly by commit, so that might be a manageable way to go through this code. - Since the updated API for this work is not yet complete, this work can be manually tested by either: - Using mock API infrastructure, or - in `ManageQueriesPage.tsx`, comment out the two `useQuery` calls and add appropriate mock data. You can then modify any fields of interest to test their related UI functionality. For example, lines 119 -242 might read: ``` // const { // data: curTeamEnhancedQueries, // error: curTeamQueriesError, // isFetching: isFetchingCurTeamQueries, // refetch: refetchCurTeamQueries, // } = useQuery( // [{ scope: "queries", teamId: teamIdForApi }], // () => queriesAPI.loadAll(teamIdForApi), // { // refetchOnWindowFocus: false, // enabled: isRouteOk, // select: (data) => data.queries.map(enhanceQuery), // } // ); // // If a team is selected, fetch inherited global queries as well // const { // data: globalEnhancedQueries, // error: globalQueriesError, // isFetching: isFetchingGlobalQueries, // refetch: refetchGlobalQueries, // } = useQuery( // [{ scope: "queries", teamId: -1 }], // () => queriesAPI.loadAll(), // { // refetchOnWindowFocus: false, // enabled: isRouteOk && isAnyTeamSelected, // select: (data) => data.queries.map(enhanceQuery), // } // ); const [ curTeamEnhancedQueries, curTeamQueriesError, isFetchingCurTeamQueries, refetchCurTeamQueries, ] = useMemo(() => { return [ [ { created_at: "2023-06-08T15:31:35Z", updated_at: "2023-06-08T15:31:35Z", id: 2, name: "test", description: "", query: "SELECT * FROM osquery_info;", team_id: 43, platform: "darwin", min_osquery_version: "", automations_enabled: true, logging: "snapshot", saved: true, // interval: 300, interval: 0, observer_can_run: false, author_id: 1, author_name: "Jacob", author_email: "jacob@fleetdm.com", packs: [], stats: { // system_time_p50: 1, // system_time_p95: null, // user_time_p50: 1, // user_time_p95: null, // total_executions: 1, }, performance: "Undetermined", platforms: ["darwin"], }, ] as IEnhancedQuery[], undefined, false, () => { console.log("got the new queries"); }, ]; }, []); const [ globalEnhancedQueries, globalQueriesError, isFetchingGlobalQueries, refetchGlobalQueries, ] = useMemo(() => { return [ [ { created_at: "2023-06-08T15:31:35Z", updated_at: "2023-06-08T15:31:35Z", id: 200, name: "test", description: "", query: "SELECT * FROM osquery_info;", team_id: null, platform: "darwin", min_osquery_version: "", automations_enabled: true, logging: "snapshot", saved: true, // interval: 300, interval: 0, observer_can_run: false, author_id: 1, author_name: "Jacob", author_email: "jacob@fleetdm.com", packs: [], stats: { // system_time_p50: 1, // system_time_p95: null, // user_time_p50: 1, // user_time_p95: null, // total_executions: 1, }, performance: "Undetermined", platforms: ["darwin"], }, ] as IEnhancedQuery[], undefined, false, () => { console.log("got the new inherited queries"); }, ]; }, []); ``` - [x] Changes file added for user-visible changes in `changes/` - [x] Manual QA for all new/changed functionality --------- Co-authored-by: Jacob Shandling --- changes/12636-merge-schedule-into-queries | 1 + .../StatusIndicator/StatusIndicator.tsx | 2 +- .../components/StatusIndicator/_styles.scss | 20 + .../DataTable/PillCell/PillCell.tests.tsx | 4 +- .../DataTable/PillCell/PillCell.tsx | 12 +- .../DataTable/TextCell/TextCell.tsx | 29 +- .../TableContainer/DataTable/_styles.scss | 6 + .../buttons/RevealButton/RevealButton.tsx | 2 +- .../components/top_nav/SiteTopNav/navItems.ts | 9 - .../HostActionsDropdown.tests.tsx | 16 - .../HostActionsDropdown/helpers.tsx | 9 +- .../HostDetailsPage/HostDetailsPage.tsx | 110 ---- .../cards/Packs/PackTable/PackTableConfig.tsx | 1 - .../hosts/details/cards/Schedule/Schedule.tsx | 80 --- .../cards/Schedule/ScheduleTableConfig.tsx | 123 ---- .../hosts/details/cards/Schedule/_styles.scss | 29 - .../hosts/details/cards/Schedule/index.ts | 1 - .../ManageQueriesPage/ManageQueriesPage.tsx | 329 ++++++++--- .../queries/ManageQueriesPage/_styles.scss | 28 +- .../AutomationsModal.tsx | 19 + .../ManageAutomationsModal.tsx | 21 + .../ManageAutomationsModal/index.ts | 1 + .../QueriesTable/QueriesTableConfig.tsx | 98 +++- .../ManageSchedulePage/ManageSchedulePage.tsx | 546 ------------------ .../schedule/ManageSchedulePage/_styles.scss | 122 ---- .../PreviewDataModal/PreviewDataModal.tsx | 61 -- .../components/PreviewDataModal/_styles.scss | 14 - .../components/PreviewDataModal/index.ts | 1 - .../RemoveScheduledQueryModal.tsx | 47 -- .../RemoveScheduledQueryModal/index.ts | 1 - .../ScheduleEditorModal.tsx | 364 ------------ .../ScheduleEditorModal/_styles.scss | 26 - .../components/ScheduleEditorModal/index.ts | 1 - .../ScheduleTable/ScheduleTable.tsx | 224 ------- .../ScheduleTable/ScheduleTableConfig.tsx | 272 --------- .../components/ScheduleTable/index.ts | 1 - .../schedule/ManageSchedulePage/index.ts | 1 - frontend/router/index.tsx | 10 - frontend/services/entities/queries.ts | 15 +- 39 files changed, 433 insertions(+), 2223 deletions(-) create mode 100644 changes/12636-merge-schedule-into-queries delete mode 100644 frontend/pages/hosts/details/cards/Schedule/Schedule.tsx delete mode 100644 frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx delete mode 100644 frontend/pages/hosts/details/cards/Schedule/_styles.scss delete mode 100644 frontend/pages/hosts/details/cards/Schedule/index.ts create mode 100644 frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/AutomationsModal.tsx create mode 100644 frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx create mode 100644 frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/index.ts delete mode 100644 frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx delete mode 100644 frontend/pages/schedule/ManageSchedulePage/_styles.scss delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/PreviewDataModal.tsx delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/_styles.scss delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/index.ts delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/index.ts delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/_styles.scss delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/index.ts delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTableConfig.tsx delete mode 100644 frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/index.ts delete mode 100644 frontend/pages/schedule/ManageSchedulePage/index.ts diff --git a/changes/12636-merge-schedule-into-queries b/changes/12636-merge-schedule-into-queries new file mode 100644 index 0000000000..bea20bbdda --- /dev/null +++ b/changes/12636-merge-schedule-into-queries @@ -0,0 +1 @@ +- Merged all functionality of the Schedule page into the Queries page diff --git a/frontend/components/StatusIndicator/StatusIndicator.tsx b/frontend/components/StatusIndicator/StatusIndicator.tsx index 7bae7226c2..7f1d81816f 100644 --- a/frontend/components/StatusIndicator/StatusIndicator.tsx +++ b/frontend/components/StatusIndicator/StatusIndicator.tsx @@ -7,7 +7,7 @@ interface IStatusIndicatorProps { value: string; tooltip?: { id: number; - tooltipText: string; + tooltipText: string | JSX.Element; position?: "top" | "bottom"; }; } diff --git a/frontend/components/StatusIndicator/_styles.scss b/frontend/components/StatusIndicator/_styles.scss index 09c490804e..d5eb1721c7 100644 --- a/frontend/components/StatusIndicator/_styles.scss +++ b/frontend/components/StatusIndicator/_styles.scss @@ -52,4 +52,24 @@ background-color: $ui-warning; } } + + // Query automations status + &--on { + &:before { + background-color: $ui-success; + } + } + &--off { + &:before { + background-color: $ui-offline; + } + } + &--paused { + .status-tooltip { + text-transform: none; + } + &:before { + background-color: $ui-offline; + } + } } diff --git a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tests.tsx b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tests.tsx index da2c39d855..2f6d5eb55e 100644 --- a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tests.tsx +++ b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tests.tsx @@ -8,9 +8,7 @@ const PERFORMANCE_IMPACT = { indicator: "Minimal", id: 3 }; describe("Pill cell", () => { it("renders pill text and tooltip on hover", async () => { - const { user } = renderWithSetup( - - ); + const { user } = renderWithSetup(); await user.hover(screen.getByText("Minimal")); diff --git a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx index 528858b357..6d81789f42 100644 --- a/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx +++ b/frontend/components/TableContainer/DataTable/PillCell/PillCell.tsx @@ -7,18 +7,13 @@ import ReactTooltip from "react-tooltip"; interface IPillCellProps { value: { indicator: string; id: number }; customIdPrefix?: string; - hostDetails?: boolean; } const generateClassTag = (rawValue: string): string => { return rawValue.replace(" ", "-").toLowerCase(); }; -const PillCell = ({ - value, - customIdPrefix, - hostDetails, -}: IPillCellProps): JSX.Element => { +const PillCell = ({ value, customIdPrefix }: IPillCellProps): JSX.Element => { const { indicator, id } = value; const pillClassName = classnames( "data-table__pill", @@ -75,9 +70,8 @@ const PillCell = ({ case "Undetermined": return ( <> - To see performance
impact, this query must
run as a - scheduled query
on {hostDetails ? "this" : "at least one"}{" "} - host. + To see performance impact, this query must have run with + automations on at least one host. ); default: diff --git a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx index 403ab3188c..3678f09b4b 100644 --- a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx +++ b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx @@ -1,4 +1,6 @@ +import { uniqueId } from "lodash"; import React from "react"; +import ReactTooltip from "react-tooltip"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; interface ITextCellProps { @@ -6,6 +8,7 @@ interface ITextCellProps { formatter?: (val: any) => JSX.Element | string; // string, number, or null greyed?: boolean; classes?: string; + emptyCellTooltipText?: JSX.Element | string; } const TextCell = ({ @@ -13,6 +16,7 @@ const TextCell = ({ formatter = (val) => val, // identity function if no formatter is provided greyed, classes = "w250", + emptyCellTooltipText, }: ITextCellProps): JSX.Element => { let val = value; @@ -22,9 +26,32 @@ const TextCell = ({ if (!val) { greyed = true; } + + const renderEmptyCell = () => { + if (emptyCellTooltipText) { + const tooltipId = uniqueId(); + return ( + <> + + {DEFAULT_EMPTY_CELL_VALUE} + + + {emptyCellTooltipText} + + + ); + } + return DEFAULT_EMPTY_CELL_VALUE; + }; + return ( - {formatter(val) || DEFAULT_EMPTY_CELL_VALUE} + {formatter(val) || renderEmptyCell()} ); }; diff --git a/frontend/components/TableContainer/DataTable/_styles.scss b/frontend/components/TableContainer/DataTable/_styles.scss index c1ce50aeb8..3a850a34b7 100644 --- a/frontend/components/TableContainer/DataTable/_styles.scss +++ b/frontend/components/TableContainer/DataTable/_styles.scss @@ -220,6 +220,9 @@ $shadow-transition-width: 10px; text-overflow: ellipsis; white-space: nowrap; margin: 0; + .__react_component_tooltip { + white-space: normal; + } } .w400 { max-width: calc(400px - 48px); @@ -234,6 +237,9 @@ $shadow-transition-width: 10px; .grey-cell { color: $ui-fleet-black-50; font-style: italic; + .__react_component_tooltip { + font-style: normal; + } } } diff --git a/frontend/components/buttons/RevealButton/RevealButton.tsx b/frontend/components/buttons/RevealButton/RevealButton.tsx index 830e56cebf..01b606f892 100644 --- a/frontend/components/buttons/RevealButton/RevealButton.tsx +++ b/frontend/components/buttons/RevealButton/RevealButton.tsx @@ -47,7 +47,7 @@ const RevealButton = ({ {caretPosition === "before" && ( )} diff --git a/frontend/components/top_nav/SiteTopNav/navItems.ts b/frontend/components/top_nav/SiteTopNav/navItems.ts index 203e14f597..fd466b07d5 100644 --- a/frontend/components/top_nav/SiteTopNav/navItems.ts +++ b/frontend/components/top_nav/SiteTopNav/navItems.ts @@ -78,15 +78,6 @@ export default ( pathname: PATHS.MANAGE_QUERIES, }, }, - { - name: "Schedule", - location: { - regex: new RegExp(`^${URL_PREFIX}/(schedule|packs)/`), - pathname: PATHS.MANAGE_SCHEDULE, - }, - exclude: !isMaintainerOrAdmin, - withParams: { type: "query", names: ["team_id"] }, - }, { name: "Policies", location: { diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx index 6110452dbe..3ac219c2e0 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tests.tsx @@ -54,22 +54,6 @@ describe("Host Actions Dropdown", () => { }); }); - it("renders the Query action as disabled if the host is offline", async () => { - const render = createCustomRenderer(); - - const { user } = render( - - ); - - await user.click(screen.getByText("Actions")); - - expect(screen.getByText("Query").parentNode).toHaveClass("is-disabled"); - }); - it("renders the Show Disk Encryption Key action when on premium tier and we store the disk encryption key", async () => { const render = createCustomRenderer({ context: { diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx index 82eede9937..a03079f7f3 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx @@ -10,11 +10,6 @@ const DEFAULT_OPTIONS: IDropdownOption[] = [ disabled: false, premiumOnly: true, }, - { - label: "Query", - value: "query", - disabled: false, - }, { label: "Show disk encryption key", value: "diskEncryption", @@ -122,9 +117,7 @@ const setOptionsAsDisabled = ( let optionsToDisable: IDropdownOption[] = []; if (!isHostOnline) { optionsToDisable = optionsToDisable.concat( - options.filter( - (option) => option.value === "query" || option.value === "mdmOff" - ) + options.filter((option) => option.value === "mdmOff") ); } if (isSandboxMode) { diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index f67a3f13d7..820915caa2 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -9,7 +9,6 @@ import { pick } from "lodash"; import PATHS from "router/paths"; import hostAPI from "services/entities/hosts"; -import queryAPI from "services/entities/queries"; import teamAPI, { ILoadTeamsResponse } from "services/entities/teams"; import { AppContext } from "context/app"; import { PolicyContext } from "context/policy"; @@ -18,14 +17,11 @@ import { IHost, IDeviceMappingResponse, IMacadminsResponse, - IPackStats, IHostResponse, IHostMdmData, } from "interfaces/host"; import { ILabel } from "interfaces/label"; import { IHostPolicy } from "interfaces/policy"; -import { IQuery, IFleetQueriesResponse } from "interfaces/query"; -import { IQueryStats } from "interfaces/query_stats"; import { ISoftware } from "interfaces/software"; import { ITeam } from "interfaces/team"; @@ -36,7 +32,6 @@ import InfoBanner from "components/InfoBanner"; import BackLink from "components/BackLink"; import { normalizeEmptyValues, wrapFleetHelper } from "utilities/helpers"; -import permissions from "utilities/permissions"; import HostSummaryCard from "../cards/HostSummary"; import AboutCard from "../cards/About"; @@ -46,9 +41,6 @@ import MunkiIssuesCard from "../cards/MunkiIssues"; import SoftwareCard from "../cards/Software"; import UsersCard from "../cards/Users"; import PoliciesCard from "../cards/Policies"; -import ScheduleCard from "../cards/Schedule"; -import PacksCard from "../cards/Packs"; -import SelectQueryModal from "./modals/SelectQueryModal"; import PolicyDetailsModal from "../cards/Policies/HostPoliciesTable/PolicyDetailsModal"; import OSPolicyModal from "./modals/OSPolicyModal"; import UnenrollMdmModal from "./modals/UnenrollMdmModal"; @@ -95,12 +87,6 @@ interface IHostDetailsSubNavItem { pathname: string; } -const TAGGED_TEMPLATES = { - queryByHostRoute: (hostId: number | undefined | null) => { - return `${hostId ? `?host_ids=${hostId}` : ""}`; - }, -}; - const HostDetailsPage = ({ route, router, @@ -113,9 +99,7 @@ const HostDetailsPage = ({ const { config, - currentUser, isGlobalAdmin = false, - isGlobalObserver, isPremiumTier = false, isSandboxMode, isOnlyObserver, @@ -135,7 +119,6 @@ const HostDetailsPage = ({ const [showDeleteHostModal, setShowDeleteHostModal] = useState(false); const [showTransferHostModal, setShowTransferHostModal] = useState(false); - const [showSelectQueryModal, setShowSelectQueryModal] = useState(false); const [showPolicyDetailsModal, setPolicyDetailsModal] = useState(false); const [showOSPolicyModal, setShowOSPolicyModal] = useState(false); const [showMacSettingsModal, setShowMacSettingsModal] = useState(false); @@ -151,26 +134,11 @@ const HostDetailsPage = ({ const [refetchStartTime, setRefetchStartTime] = useState(null); const [showRefetchSpinner, setShowRefetchSpinner] = useState(false); - const [packsState, setPacksState] = useState(); - const [schedule, setSchedule] = useState(); const [hostSoftware, setHostSoftware] = useState([]); const [usersState, setUsersState] = useState<{ username: string }[]>([]); const [usersSearchString, setUsersSearchString] = useState(""); const [pathname, setPathname] = useState(""); - const { data: fleetQueries, error: fleetQueriesError } = useQuery< - IFleetQueriesResponse, - Error, - IQuery[] - >("fleet queries", () => queryAPI.loadAll(), { - enabled: !!hostIdFromURL, - refetchOnMount: false, - refetchOnReconnect: false, - refetchOnWindowFocus: false, - retry: false, - select: (data: IFleetQueriesResponse) => data.queries, - }); - const { data: teams } = useQuery( "teams", () => teamAPI.loadAll(), @@ -294,27 +262,6 @@ const HostDetailsPage = ({ } setHostSoftware(returnedHost.software || []); setUsersState(returnedHost.users || []); - if (returnedHost.pack_stats) { - const packStatsByType = returnedHost.pack_stats.reduce( - ( - dictionary: { - packs: IPackStats[]; - schedule: IQueryStats[]; - }, - pack: IPackStats - ) => { - if (pack.type === "pack") { - dictionary.packs.push(pack); - } else { - dictionary.schedule.push(...pack.query_stats); - } - return dictionary; - }, - { packs: [], schedule: [] } - ); - setPacksState(packStatsByType.packs); - setSchedule(packStatsByType.schedule); - } }, onError: (error) => handlePageError(error), } @@ -480,17 +427,6 @@ const HostDetailsPage = ({ : router.push(PATHS.MANAGE_HOSTS_LABEL(label.id)); }; - const onQueryHostCustom = () => { - router.push(PATHS.NEW_QUERY + TAGGED_TEMPLATES.queryByHostRoute(host?.id)); - }; - - const onQueryHostSaved = (selectedQuery: IQuery) => { - router.push( - PATHS.EDIT_QUERY(selectedQuery) + - TAGGED_TEMPLATES.queryByHostRoute(host?.id) - ); - }; - const onTransferHostSubmit = async (team: ITeam) => { setIsUpdatingHost(true); @@ -528,9 +464,6 @@ const HostDetailsPage = ({ case "transfer": setShowTransferHostModal(true); break; - case "query": - setShowSelectQueryModal(true); - break; case "diskEncryption": setShowDiskEncryptionModal(true); break; @@ -576,11 +509,6 @@ const HostDetailsPage = ({ title: "software", pathname: PATHS.HOST_SOFTWARE(hostIdFromURL), }, - { - name: "Schedule", - title: "schedule", - pathname: PATHS.HOST_SCHEDULE(hostIdFromURL), - }, { name: ( <> @@ -615,23 +543,6 @@ const HostDetailsPage = ({ host?.mdm.name === "Fleet" && host?.mdm.macos_settings?.disk_encryption === "action_required"; - /* Context team id might be different that host's team id - Observer plus must be checked against host's team id */ - const isGlobalOrHostsTeamObserverPlus = - currentUser && host?.team_id - ? permissions.isObserverPlus(currentUser, host.team_id) - : false; - - const isHostsTeamObserver = - currentUser && host?.team_id - ? permissions.isTeamObserver(currentUser, host.team_id) - : false; - - const canViewPacks = - !isGlobalObserver && - !isGlobalOrHostsTeamObserverPlus && - !isHostsTeamObserver; - const bootstrapPackageData = { status: host?.mdm.macos_setup?.bootstrap_package_status, details: host?.mdm.macos_setup?.details, @@ -738,16 +649,6 @@ const HostDetailsPage = ({ /> )} - - - {canViewPacks && ( - - )} - )} - {showSelectQueryModal && host && ( - setShowSelectQueryModal(false)} - queries={fleetQueries || []} - queryErrors={fleetQueriesError} - isOnlyObserver={isOnlyObserver} - onQueryHostCustom={onQueryHostCustom} - onQueryHostSaved={onQueryHostSaved} - hostsTeamId={host?.team_id} - /> - )} {!!host && showTransferHostModal && ( setShowTransferHostModal(false)} diff --git a/frontend/pages/hosts/details/cards/Packs/PackTable/PackTableConfig.tsx b/frontend/pages/hosts/details/cards/Packs/PackTable/PackTableConfig.tsx index 928074d219..36ca84584a 100644 --- a/frontend/pages/hosts/details/cards/Packs/PackTable/PackTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Packs/PackTable/PackTableConfig.tsx @@ -104,7 +104,6 @@ const generatePackTableHeaders = (): IDataColumn[] => { ), }, diff --git a/frontend/pages/hosts/details/cards/Schedule/Schedule.tsx b/frontend/pages/hosts/details/cards/Schedule/Schedule.tsx deleted file mode 100644 index 1c37493510..0000000000 --- a/frontend/pages/hosts/details/cards/Schedule/Schedule.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import React from "react"; - -import { IQueryStats } from "interfaces/query_stats"; -import TableContainer from "components/TableContainer"; -import EmptyTable from "components/EmptyTable"; -import CustomLink from "components/CustomLink"; - -import { generateTableHeaders, generateDataSet } from "./ScheduleTableConfig"; - -const baseClass = "schedule"; - -interface IScheduleProps { - schedule?: IQueryStats[]; - isChromeOSHost: boolean; - isLoading: boolean; -} - -const Schedule = ({ - schedule, - isChromeOSHost, - isLoading, -}: IScheduleProps): JSX.Element => { - const wrapperClassName = `${baseClass}__pack-table`; - const tableHeaders = generateTableHeaders(); - - const renderEmptyScheduleTab = () => { - if (isChromeOSHost) { - return ( - - Interested in collecting data from your Chromebooks? - - - } - /> - ); - } - return ( - - ); - }; - - return ( -
-

Schedule

- {!schedule || !schedule.length || isChromeOSHost ? ( - renderEmptyScheduleTab() - ) : ( -
- null} - resultsTitle={"queries"} - defaultSortHeader={"scheduled_query_name"} - defaultSortDirection={"asc"} - showMarkAllPages={false} - isAllPagesSelected={false} - emptyComponent={() => <>} - disablePagination - disableCount - /> -
- )} -
- ); -}; - -export default Schedule; diff --git a/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx b/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx deleted file mode 100644 index c02374741b..0000000000 --- a/frontend/pages/hosts/details/cards/Schedule/ScheduleTableConfig.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import React from "react"; - -import { IQueryStats } from "interfaces/query_stats"; -import { performanceIndicator, secondsToDhms } from "utilities/helpers"; - -import TextCell from "components/TableContainer/DataTable/TextCell"; -import PillCell from "components/TableContainer/DataTable/PillCell"; -import TooltipWrapper from "components/TooltipWrapper"; - -interface IHeaderProps { - column: { - title: string; - isSortedDesc: boolean; - }; -} - -interface IRowProps { - row: { - original: IQueryStats; - }; -} - -interface ICellProps extends IRowProps { - cell: { - value: string | number | boolean; - }; -} - -interface IPillCellProps extends IRowProps { - cell: { - value: { - indicator: string; - id: number; - }; - }; -} - -interface IDataColumn { - title?: string; - Header: ((props: IHeaderProps) => JSX.Element) | string; - accessor: string; - Cell: - | ((props: ICellProps) => JSX.Element) - | ((props: IPillCellProps) => JSX.Element); - disableHidden?: boolean; - disableSortBy?: boolean; -} - -interface IScheduleTable extends Partial { - frequency: string; - performance: { indicator: string; id: number }; -} - -// NOTE: cellProps come from react-table -// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties -const generateTableHeaders = (): IDataColumn[] => { - return [ - { - title: "Query", - Header: "Query", - disableSortBy: true, - accessor: "query_name", - Cell: (cellProps: ICellProps) => ( - - ), - }, - { - title: "Frequency", - Header: "Frequency", - disableSortBy: true, - accessor: "frequency", - Cell: (cellProps: ICellProps) => ( - - ), - }, - { - Header: () => { - return ( - - Performance impact - - ); - }, - disableSortBy: true, - accessor: "performance", - Cell: (cellProps: IPillCellProps) => ( - - ), - }, - ]; -}; - -const enhanceScheduleData = (query_stats: IQueryStats[]): IScheduleTable[] => { - return Object.values(query_stats).map((query) => { - const scheduledQueryPerformance = { - user_time_p50: query.user_time, - system_time_p50: query.system_time, - total_executions: query.executions, - }; - return { - query_name: query.query_name, - frequency: secondsToDhms(query.interval), - performance: { - indicator: performanceIndicator(scheduledQueryPerformance), - id: query.scheduled_query_id, - }, - }; - }); -}; - -const generateDataSet = (query_stats: IQueryStats[]): IScheduleTable[] => { - if (!query_stats) { - return query_stats; - } - - return [...enhanceScheduleData(query_stats)]; -}; - -export { generateTableHeaders, generateDataSet }; diff --git a/frontend/pages/hosts/details/cards/Schedule/_styles.scss b/frontend/pages/hosts/details/cards/Schedule/_styles.scss deleted file mode 100644 index 4841674726..0000000000 --- a/frontend/pages/hosts/details/cards/Schedule/_styles.scss +++ /dev/null @@ -1,29 +0,0 @@ -.section--schedule { - margin-top: $pad-medium; - .section__header { - margin-bottom: $pad-medium; - } - .table-container__header { - display: none; - } - .data-table-block { - .data-table__table { - thead { - .query_name__header { - width: $col-lg; - } - .frequency__header { - width: $col-md; - } - } - tbody { - .query_name__cell { - width: $col-lg; - } - .frequency__cell { - width: $col-md; - } - } - } - } -} diff --git a/frontend/pages/hosts/details/cards/Schedule/index.ts b/frontend/pages/hosts/details/cards/Schedule/index.ts deleted file mode 100644 index 39250f5640..0000000000 --- a/frontend/pages/hosts/details/cards/Schedule/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./Schedule"; diff --git a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx index f6d46297ff..0f36a049c3 100644 --- a/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx +++ b/frontend/pages/queries/ManageQueriesPage/ManageQueriesPage.tsx @@ -5,7 +5,7 @@ import React, { useMemo, useState, } from "react"; -import { RouteProps, InjectedRouter } from "react-router"; +import { InjectedRouter } from "react-router"; import { useQuery } from "react-query"; import { pick } from "lodash"; @@ -14,8 +14,11 @@ import { TableContext } from "context/table"; import { NotificationContext } from "context/notification"; import { performanceIndicator } from "utilities/helpers"; import { IOsqueryPlatform } from "interfaces/platform"; -import { IQuery, IFleetQueriesResponse } from "interfaces/query"; -import fleetQueriesAPI from "services/entities/queries"; +import { + IListQueriesResponse, + ISchedulableQuery, +} from "interfaces/schedulable_query"; +import queriesAPI from "services/entities/queries"; import PATHS from "router/paths"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; import checkPlatformCompatibility from "utilities/sql_tools"; @@ -23,27 +26,31 @@ import Button from "components/buttons/Button"; import Spinner from "components/Spinner"; import TableDataError from "components/DataError"; import MainContent from "components/MainContent"; +import TeamsDropdown from "components/TeamsDropdown"; +import useTeamIdParam from "hooks/useTeamIdParam"; +import RevealButton from "components/buttons/RevealButton"; import QueriesTable from "./components/QueriesTable"; import DeleteQueryModal from "./components/DeleteQueryModal"; +import ManageAutomationsModal from "./components/ManageAutomationsModal"; const baseClass = "manage-queries-page"; interface IManageQueriesPageProps { - route: RouteProps; router: InjectedRouter; // v3 location: { - pathname?: string; + pathname: string; query: { platform?: string; page?: string; query?: string; order_key?: string; order_direction?: "asc" | "desc"; + team_id?: string; }; search: string; }; } -interface IQueryTableData extends IQuery { +interface IEnhancedQuery extends ISchedulableQuery { performance: string; platforms: string[]; } @@ -54,7 +61,7 @@ const getPlatforms = (queryString: string): Array => { return platforms || [DEFAULT_EMPTY_CELL_VALUE]; }; -const enhanceQuery = (q: IQuery) => { +const enhanceQuery = (q: ISchedulableQuery): IEnhancedQuery => { return { ...q, performance: performanceIndicator( @@ -71,51 +78,74 @@ const ManageQueriesPage = ({ const queryParams = location.query; const { + isGlobalAdmin, + isTeamAdmin, isOnlyObserver, isObserverPlus, isAnyTeamObserverPlus, + isOnGlobalTeam, setFilteredQueriesPath, filteredQueriesPath, + isPremiumTier, + isSandboxMode, } = useContext(AppContext); const { setResetSelectedRows } = useContext(TableContext); const { renderFlash } = useContext(NotificationContext); - const [queriesList, setQueriesList] = useState( - null - ); + const { + userTeams, + currentTeamId, + handleTeamChange, + teamIdForApi, + isRouteOk, + } = useTeamIdParam({ + location, + router, + includeAllTeams: true, + includeNoTeam: false, + }); + + const isAnyTeamSelected = currentTeamId !== -1; + const [selectedQueryIds, setSelectedQueryIds] = useState([]); const [showDeleteQueryModal, setShowDeleteQueryModal] = useState(false); const [isUpdatingQueries, setIsUpdatingQueries] = useState(false); + const [showManageAutomationsModal, setShowManageAutomationsModal] = useState( + false + ); + const [showInheritedQueries, setShowInheritedQueries] = useState(false); const { - data: fleetQueries, - error: fleetQueriesError, - isFetching: isFetchingFleetQueries, - refetch: refetchFleetQueries, - } = useQuery( - "fleet queries by platform", - () => fleetQueriesAPI.loadAll(), + data: curTeamEnhancedQueries, + error: curTeamQueriesError, + isFetching: isFetchingCurTeamQueries, + refetch: refetchCurTeamQueries, + } = useQuery( + [{ scope: "queries", teamId: teamIdForApi }], + () => queriesAPI.loadAll(teamIdForApi), { refetchOnWindowFocus: false, - select: (data: IFleetQueriesResponse) => data.queries, + enabled: isRouteOk, + select: (data) => data.queries.map(enhanceQuery), } ); - const enhancedQueriesList = useMemo(() => { - const enhancedQueries = fleetQueries?.map((q: IQuery) => { - const query = enhanceQuery(q); - return query; - }); - - return enhancedQueries || []; - }, [fleetQueries]); - - useEffect(() => { - if (!isFetchingFleetQueries && enhancedQueriesList) { - setQueriesList(enhancedQueriesList); + // If a team is selected, fetch inherited global queries as well + const { + data: globalEnhancedQueries, + error: globalQueriesError, + isFetching: isFetchingGlobalQueries, + refetch: refetchGlobalQueries, + } = useQuery( + [{ scope: "queries", teamId: -1 }], + () => queriesAPI.loadAll(), + { + refetchOnWindowFocus: false, + enabled: isRouteOk && isAnyTeamSelected, + select: (data) => data.queries.map(enhanceQuery), } - }, [enhancedQueriesList, isFetchingFleetQueries]); + ); useEffect(() => { const path = location.pathname + location.search; @@ -130,85 +160,152 @@ const ManageQueriesPage = ({ setShowDeleteQueryModal(!showDeleteQueryModal); }, [showDeleteQueryModal, setShowDeleteQueryModal]); + const toggleManageAutomationsModal = useCallback(() => { + setShowManageAutomationsModal(!showManageAutomationsModal); + }, [showManageAutomationsModal, setShowManageAutomationsModal]); + const onDeleteQueryClick = (selectedTableQueryIds: number[]) => { toggleDeleteQueryModal(); setSelectedQueryIds(selectedTableQueryIds); }; - const onDeleteQuerySubmit = useCallback(async () => { - const queryOrQueries = selectedQueryIds.length === 1 ? "query" : "queries"; + const refetchAllQueries = useCallback(() => { + refetchCurTeamQueries(); + refetchGlobalQueries(); + }, [refetchCurTeamQueries, refetchGlobalQueries]); + const onDeleteQuerySubmit = useCallback(async () => { + const bulk = selectedQueryIds.length > 1; setIsUpdatingQueries(true); - const deleteQueries = selectedQueryIds.map((id) => - fleetQueriesAPI.destroy(id) - ); - try { - await Promise.all(deleteQueries).then(() => { - renderFlash("success", `Successfully deleted ${queryOrQueries}.`); - setResetSelectedRows(true); - refetchFleetQueries(); - }); - renderFlash("success", `Successfully deleted ${queryOrQueries}.`); + if (bulk) { + await queriesAPI.bulkDestroy(selectedQueryIds); + } else { + await queriesAPI.destroy(selectedQueryIds[0]); + } + renderFlash( + "success", + `Successfully deleted ${bulk ? "queries" : "query"}.` + ); + setResetSelectedRows(true); + refetchAllQueries(); } catch (errorResponse) { renderFlash( "error", - `There was an error deleting your ${queryOrQueries}. Please try again later.` + `There was an error deleting your ${ + bulk ? "queries" : "query" + }. Please try again later.` ); } finally { toggleDeleteQueryModal(); setIsUpdatingQueries(false); } - }, [refetchFleetQueries, selectedQueryIds, toggleDeleteQueryModal]); + }, [refetchAllQueries, selectedQueryIds, toggleDeleteQueryModal]); - const isTableDataLoading = isFetchingFleetQueries || queriesList === null; - - return ( - -
-
-
-
-

- Queries -

-
-
- {(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) && - !!fleetQueries?.length && ( -
- -
- )} -
-
-

Manage queries to ask specific questions about your devices.

-
-
- {isTableDataLoading && !fleetQueriesError && } - {!isTableDataLoading && fleetQueriesError ? ( - - ) : ( - { + if (isPremiumTier) { + if (userTeams) { + if (userTeams.length > 1 || isOnGlobalTeam) { + return ( + - )} -
+ ); + } else if (!isOnGlobalTeam && userTeams.length === 1) { + return

{userTeams[0].name}

; + } + } + } + return

Queries

; + }; + + const renderCurrentScopeQueriesTable = () => { + if (isFetchingCurTeamQueries) { + return ; + } + if (curTeamQueriesError) { + return ; + } + return ( +
+ +
+ ); + }; + + const renderShowInheritedQueriesTableButton = () => { + const inheritedQueryCount = globalEnhancedQueries?.length; + return ( + schedule run on this team’s hosts.' + } + onClick={() => { + setShowInheritedQueries(!showInheritedQueries); + }} + /> + ); + }; + + const renderInheritedQueriesTable = () => { + if (isFetchingGlobalQueries) { + return ; + } + if (globalQueriesError) { + return ; + } + return ( +
+ +
+ ); + }; + + const renderInheritedQueriesSection = () => { + return ( + <> + {renderShowInheritedQueriesTableButton()} + {showInheritedQueries && renderInheritedQueriesTable()} + + ); + }; + + const renderModals = () => { + return ( + <> {showDeleteQueryModal && ( )} + {showManageAutomationsModal && ( + + )} + + ); + }; + + return ( + +
+
+
+
+
{renderHeader()}
+
+
+
+ {(isGlobalAdmin || isTeamAdmin) && ( + + )} + {(!isOnlyObserver || isObserverPlus || isAnyTeamObserverPlus) && + !!curTeamEnhancedQueries?.length && ( + + )} +
+
+
+

+ Manage and schedule queries to ask questions and collect telemetry + for all hosts{isAnyTeamSelected && " assigned to this team"}. +

+
+ {renderCurrentScopeQueriesTable()} + {isAnyTeamSelected && + globalEnhancedQueries && + globalEnhancedQueries?.length > 0 && + renderInheritedQueriesSection()} + {renderModals()}
); diff --git a/frontend/pages/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss index d88be01d9c..6aee329207 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -56,7 +56,7 @@ &__action-button-container { display: flex; - align-items: flex-start; + gap: $pad-small; } .form-field--dropdown { @@ -103,11 +103,11 @@ .platforms__header { width: $col-sm; } - .author_name__header { + .updated_at__header { display: none; width: 0; } - .updated_at__header { + .performance__header { display: none; width: 0; } @@ -151,28 +151,16 @@ .platforms__cell { max-width: $col-md; } - .author_name__cell { - display: none; - max-width: $col-md; - img, - div, - span { - display: flex; - align-items: center; - } - div { - padding-right: $pad-small; - } - .author-name { - display: block; - } - } .updated_at__cell { display: none; max-width: $col-md; } + .performance__cell { + display: none; + max-width: $col-md; + } @media (min-width: $break-md) { - .author_name__cell { + .performance__cell { display: table-cell; } } diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/AutomationsModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/AutomationsModal.tsx new file mode 100644 index 0000000000..ff49667d43 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/AutomationsModal.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +import Modal from "components/Modal"; + +const baseClass = "automations-modal"; + +interface IAutomationsModalProps { + onExit: () => void; +} + +const AutomationsModal = ({ onExit }: IAutomationsModalProps): JSX.Element => { + return ( + +
+ + ); +}; + +export default AutomationsModal; diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx new file mode 100644 index 0000000000..cb6abd9cb8 --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/ManageAutomationsModal.tsx @@ -0,0 +1,21 @@ +import React from "react"; + +import Modal from "components/Modal"; + +const baseClass = "automations-modal"; + +interface IManageAutomationsModalProps { + onExit: () => void; +} + +const ManageAutomationsModal = ({ + onExit, +}: IManageAutomationsModalProps): JSX.Element => { + return ( + +
+ + ); +}; + +export default ManageAutomationsModal; diff --git a/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/index.ts b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/index.ts new file mode 100644 index 0000000000..c9128e3c2d --- /dev/null +++ b/frontend/pages/queries/ManageQueriesPage/components/ManageAutomationsModal/index.ts @@ -0,0 +1 @@ +export { default } from "./ManageAutomationsModal"; diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx index 26e5bcc58f..a728da89a1 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx @@ -7,12 +7,11 @@ import formatDistanceToNow from "date-fns/formatDistanceToNow"; import PATHS from "router/paths"; import permissionsUtils from "utilities/permissions"; -import { IQuery } from "interfaces/query"; import { IUser } from "interfaces/user"; -import { addGravatarUrlToResource } from "utilities/helpers"; +import { secondsToDhms } from "utilities/helpers"; +import { ISchedulableQuery } from "interfaces/schedulable_query"; import Icon from "components/Icon"; -import Avatar from "components/Avatar"; import Checkbox from "components/forms/fields/Checkbox"; import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell"; import HeaderCell from "components/TableContainer/DataTable/HeaderCell/HeaderCell"; @@ -20,10 +19,11 @@ import PlatformCell from "components/TableContainer/DataTable/PlatformCell"; import TextCell from "components/TableContainer/DataTable/TextCell"; import PillCell from "components/TableContainer/DataTable/PillCell"; import TooltipWrapper from "components/TooltipWrapper"; +import StatusIndicator from "components/StatusIndicator"; interface IQueryRow { id: string; - original: IQuery; + original: ISchedulableQuery; } interface IGetToggleAllRowsSelectedProps { @@ -46,7 +46,7 @@ interface IHeaderProps { } interface IRowProps { row: { - original: IQuery; + original: ISchedulableQuery; getToggleRowSelectedProps: () => IGetToggleAllRowsSelectedProps; toggleRowSelected: () => void; }; @@ -54,6 +54,18 @@ interface IRowProps { } interface ICellProps extends IRowProps { + cell: { + value: string | number | boolean; + }; +} + +interface INumberCellProps extends IRowProps { + cell: { + value: number; + }; +} + +interface IStringCellProps extends IRowProps { cell: { value: string; }; @@ -69,7 +81,9 @@ interface IDataColumn { Header: ((props: IHeaderProps) => JSX.Element) | string; Cell: | ((props: ICellProps) => JSX.Element) - | ((props: IPlatformCellProps) => JSX.Element); + | ((props: IPlatformCellProps) => JSX.Element) + | ((props: IStringCellProps) => JSX.Element) + | ((props: INumberCellProps) => JSX.Element); id?: string; title?: string; accessor?: string; @@ -148,28 +162,26 @@ const generateTableHeaders = ({ }, }, { - title: "Author", - Header: (cellProps) => ( - - ), - accessor: "author_name", - Cell: (cellProps: ICellProps): JSX.Element => { - const { author_name, author_email } = cellProps.row.original; - const author = author_name === currentUser.name ? "You" : author_name; + title: "Frequency", + Header: "Frequency", + disableSortBy: true, + accessor: "interval", + Cell: (cellProps: INumberCellProps): JSX.Element => { + const val = cellProps.cell.value + ? `Every ${secondsToDhms(cellProps.cell.value)}` + : undefined; return ( - - - {author} - + + Assign a frequency and turn automations on to + collect data at an interval. + + } + /> ); }, - sortType: "caseInsensitive", }, { Header: () => { @@ -189,7 +201,7 @@ const generateTableHeaders = ({ }, disableSortBy: true, accessor: "performance", - Cell: (cellProps: ICellProps) => ( + Cell: (cellProps: IStringCellProps) => ( ), }, + { + title: "Automations", + Header: "Automations", + disableSortBy: true, + accessor: "automations_enabled", + Cell: (cellProps: IStringCellProps): JSX.Element => { + let status; + if (cellProps.cell.value) { + if (cellProps.row.original.interval === 0) { + status = "paused"; + } else { + status = "on"; + } + } else { + status = "off"; + } + + const tooltip = + status === "paused" + ? { + id: cellProps.row.original.id, + tooltipText: ( + <> + Automations will resume for this query when + a frequency is set. + + ), + } + : undefined; + return ; + }, + }, { title: "Last modified", Header: (cellProps) => ( @@ -207,7 +251,7 @@ const generateTableHeaders = ({ /> ), accessor: "updated_at", - Cell: (cellProps: ICellProps): JSX.Element => ( + Cell: (cellProps: INumberCellProps): JSX.Element => ( void, - onEditScheduledQueryClick: (selectedQuery: IEditScheduledQuery) => void, - onShowQueryClick: (selectedQuery: IEditScheduledQuery) => void, - allScheduledQueriesList: IScheduledQuery[], - allScheduledQueriesError: Error | null, - toggleScheduleEditorModal: () => void, - isOnGlobalTeam: boolean, - selectedTeamData: ITeam | undefined, - isLoadingGlobalScheduledQueries: boolean, - isLoadingTeamScheduledQueries: boolean, - errorQueries: Error | null -): JSX.Element => { - return allScheduledQueriesError || errorQueries ? ( - - ) : ( - - ); -}; - -const renderAllTeamsTable = ( - router: InjectedRouter, - allTeamsScheduledQueriesList: IScheduledQuery[], - allTeamsScheduledQueriesError: Error | null, - isOnGlobalTeam: boolean, - selectedTeamData: ITeam | undefined, - isLoadingGlobalScheduledQueries: boolean, - isLoadingTeamScheduledQueries: boolean -): JSX.Element => { - return allTeamsScheduledQueriesError ? ( - - ) : ( -
- -
- ); -}; - -interface IFormData { - interval: number; - name?: string; - shard: number; - query?: string; - query_id?: number; - logging_type: string; - platform: string; - version: string; - team_id?: number; -} - -interface ITeamSchedulesPageProps { - params: { - team_id: string; - }; - router: InjectedRouter; // v3 - route: any; - location: any; -} - -const ManageSchedulePage = ({ - router, - location, -}: ITeamSchedulesPageProps): JSX.Element => { - const { renderFlash } = useContext(NotificationContext); - const { MANAGE_PACKS } = paths; - const handleAdvanced = () => router.push(MANAGE_PACKS); - - const { - isOnGlobalTeam, - isPremiumTier, - isFreeTier, - isSandboxMode, - } = useContext(AppContext); - - const { - currentTeamId, - isAnyTeamSelected, - isRouteOk, - teamIdForApi, - userTeams, - handleTeamChange, - } = useTeamIdParam({ - location, - router, - includeAllTeams: true, - includeNoTeam: false, - permittedAccessByTeamRole: { - admin: true, - maintainer: true, - observer: false, - observer_plus: false, - }, - }); - - const { data: teams, isLoading: isLoadingTeams } = useQuery< - ILoadTeamsResponse, - Error, - ITeam[] - >(["teams"], () => teamsAPI.loadAll(), { - enabled: isRouteOk && !!isPremiumTier, - refetchOnMount: false, - refetchOnWindowFocus: false, - select: (data) => data.teams, - }); - - const { - data: fleetQueries, - isLoading: isLoadingFleetQueries, - error: errorQueries, - } = useQuery( - ["fleetQueries"], - () => fleetQueriesAPI.loadAll(), - { - enabled: isRouteOk, - refetchOnMount: false, - refetchOnWindowFocus: false, - select: (data) => data.queries, - } - ); - - const { - data: globalScheduledQueries, - error: globalScheduledQueriesError, - isLoading: isLoadingGlobalScheduledQueries, - refetch: refetchGlobalScheduledQueries, - } = useQuery< - ILoadAllGlobalScheduledQueriesResponse, - Error, - IScheduledQuery[] - >(["globalScheduledQueries"], () => globalScheduledQueriesAPI.loadAll(), { - enabled: isRouteOk, - select: (data) => data.global_schedule, - }); - - const { - data: teamScheduledQueries, - error: teamScheduledQueriesError, - isLoading: isLoadingTeamScheduledQueries, - refetch: refetchTeamScheduledQueries, - } = useQuery( - ["teamScheduledQueries", teamIdForApi], - () => teamScheduledQueriesAPI.loadAll(teamIdForApi), - { - enabled: isRouteOk && isPremiumTier && !!teamIdForApi, - select: (data) => data.scheduled, - } - ); - - const refetchScheduledQueries = useCallback(() => { - refetchGlobalScheduledQueries(); - if (isAnyTeamSelected) { - refetchTeamScheduledQueries(); - } - }, [ - isAnyTeamSelected, - refetchGlobalScheduledQueries, - refetchTeamScheduledQueries, - ]); - - const allScheduledQueriesList = - (isAnyTeamSelected ? teamScheduledQueries : globalScheduledQueries) || []; - const allScheduledQueriesError = isAnyTeamSelected - ? teamScheduledQueriesError - : globalScheduledQueriesError; - - const inheritedScheduledQueriesList = globalScheduledQueries; - const inheritedScheduledQueriesError = globalScheduledQueriesError; - - const inheritedQueryOrQueries = - inheritedScheduledQueriesList?.length === 1 ? "query" : "queries"; - - const selectedTeamData = isAnyTeamSelected - ? teams?.find((team: ITeam) => teamIdForApi === team.id) - : undefined; - - const [isUpdatingScheduledQuery, setIsUpdatingScheduledQuery] = useState( - false - ); - const [showInheritedQueries, setShowInheritedQueries] = useState(false); - const [showScheduleEditorModal, setShowScheduleEditorModal] = useState(false); - const [showShowQueryModal, setShowShowQueryModal] = useState(false); - const [showPreviewDataModal, setShowPreviewDataModal] = useState(false); - const [ - showRemoveScheduledQueryModal, - setShowRemoveScheduledQueryModal, - ] = useState(false); - const [selectedQueryIds, setSelectedQueryIds] = useState( - [] - ); - const [ - selectedScheduledQuery, - setSelectedScheduledQuery, - ] = useState(); - - const toggleInheritedQueries = () => { - setShowInheritedQueries(!showInheritedQueries); - }; - - const togglePreviewDataModal = useCallback(() => { - setShowPreviewDataModal(!showPreviewDataModal); - }, [setShowPreviewDataModal, showPreviewDataModal]); - - const toggleScheduleEditorModal = useCallback(() => { - setSelectedScheduledQuery(undefined); // create modal renders - setShowScheduleEditorModal(!showScheduleEditorModal); - }, [showScheduleEditorModal, setShowScheduleEditorModal]); - - const toggleShowQueryModal = useCallback(() => { - setSelectedScheduledQuery(undefined); - setShowShowQueryModal(!showShowQueryModal); - }, [showShowQueryModal, setShowShowQueryModal]); - - const toggleRemoveScheduledQueryModal = useCallback(() => { - setShowRemoveScheduledQueryModal(!showRemoveScheduledQueryModal); - }, [showRemoveScheduledQueryModal, setShowRemoveScheduledQueryModal]); - - const onRemoveScheduledQueryClick = ( - selectedTableQueryIds: number[] - ): void => { - toggleRemoveScheduledQueryModal(); - setSelectedQueryIds(selectedTableQueryIds); - }; - - const onShowQueryClick = (selectedQuery: IEditScheduledQuery): void => { - toggleShowQueryModal(); - setSelectedScheduledQuery(selectedQuery); - }; - - const onEditScheduledQueryClick = ( - selectedQuery: IEditScheduledQuery - ): void => { - toggleScheduleEditorModal(); - setSelectedScheduledQuery(selectedQuery); // edit modal renders - }; - - const onRemoveScheduledQuerySubmit = useCallback(() => { - setIsUpdatingScheduledQuery(true); - const promises = selectedQueryIds.map((id: number) => { - return isAnyTeamSelected - ? teamScheduledQueriesAPI.destroy(teamIdForApi, id) - : globalScheduledQueriesAPI.destroy({ id }); - }); - const queryOrQueries = selectedQueryIds.length === 1 ? "query" : "queries"; - return Promise.all(promises) - .then(() => { - renderFlash( - "success", - `Successfully removed scheduled ${queryOrQueries}.` - ); - toggleRemoveScheduledQueryModal(); - refetchScheduledQueries(); - }) - .catch(() => { - renderFlash( - "error", - `Unable to remove scheduled ${queryOrQueries}. Please try again.` - ); - toggleRemoveScheduledQueryModal(); - }) - .finally(() => { - refetchGlobalScheduledQueries(); - setIsUpdatingScheduledQuery(false); - }); - }, [ - selectedQueryIds, - isAnyTeamSelected, - teamIdForApi, - renderFlash, - toggleRemoveScheduledQueryModal, - refetchScheduledQueries, - refetchGlobalScheduledQueries, - ]); - - const onAddScheduledQuerySubmit = useCallback( - (formData: IFormData, editQuery: IEditScheduledQuery | undefined) => { - setIsUpdatingScheduledQuery(true); - if (editQuery) { - const updatedAttributes = deepDifference(formData, editQuery); - - const editResponse = - editQuery.type === "team_scheduled_query" - ? teamScheduledQueriesAPI.update(editQuery, updatedAttributes) - : globalScheduledQueriesAPI.update(editQuery, updatedAttributes); - - editResponse - .then(() => { - renderFlash( - "success", - `Successfully updated ${formData.name} in the schedule.` - ); - refetchScheduledQueries(); - toggleScheduleEditorModal(); - }) - .catch(() => { - renderFlash( - "error", - "Could not update scheduled query. Please try again." - ); - }) - .finally(() => { - setIsUpdatingScheduledQuery(false); - refetchGlobalScheduledQueries(); - }); - } else { - const createResponse = isAnyTeamSelected - ? teamScheduledQueriesAPI.create({ ...formData }) - : globalScheduledQueriesAPI.create({ ...formData }); - - createResponse - .then(() => { - renderFlash( - "success", - `Successfully added ${formData.name} to the schedule.` - ); - refetchScheduledQueries(); - toggleScheduleEditorModal(); - }) - .catch(() => { - renderFlash("error", "Could not schedule query. Please try again."); - }) - .finally(() => { - setIsUpdatingScheduledQuery(false); - refetchGlobalScheduledQueries(); - }); - } - }, - [ - isAnyTeamSelected, - refetchGlobalScheduledQueries, - refetchScheduledQueries, - renderFlash, - toggleScheduleEditorModal, - ] - ); - - if (!isRouteOk || (isPremiumTier && !userTeams?.length)) { - return ( -
- -
- ); - } - - return ( - -
-
-
-
-
- {isFreeTier &&

Schedule

} - {isPremiumTier && - userTeams && - (userTeams.length > 1 || isOnGlobalTeam) && ( - - )} - {isPremiumTier && - !isOnGlobalTeam && - userTeams && - userTeams.length === 1 &&

{userTeams[0].name}

} -
-
-
- {allScheduledQueriesList?.length !== 0 && !allScheduledQueriesError && ( -
- {/* NOTE: Product decision to remove packs from UI - {isOnGlobalTeam && ( - - )} */} - -
- )} -
-
- {!isLoadingTeams && ( -
- {isAnyTeamSelected ? ( -

- Schedule queries for{" "} - all hosts assigned to this team -

- ) : ( -

- Schedule queries to run at regular intervals across{" "} - all of your hosts -

- )} -
- )} -
-
- {isLoadingTeams || - isLoadingFleetQueries || - isLoadingGlobalScheduledQueries || - isLoadingTeamScheduledQueries ? ( - - ) : ( - renderTable( - router, - onRemoveScheduledQueryClick, - onEditScheduledQueryClick, - onShowQueryClick, - allScheduledQueriesList, - allScheduledQueriesError, - toggleScheduleEditorModal, - isOnGlobalTeam || false, - selectedTeamData, - isLoadingGlobalScheduledQueries, - isLoadingTeamScheduledQueries, - errorQueries - ) - )} -
- {/* must use ternary for NaN */} - {isAnyTeamSelected && - inheritedScheduledQueriesList && - inheritedScheduledQueriesList.length > 0 ? ( - schedule run on this team’s hosts.' - } - onClick={toggleInheritedQueries} - /> - ) : null} - {showInheritedQueries && - inheritedScheduledQueriesList && - renderAllTeamsTable( - router, - inheritedScheduledQueriesList, - inheritedScheduledQueriesError, - isOnGlobalTeam || false, - selectedTeamData, - isLoadingGlobalScheduledQueries, - isLoadingTeamScheduledQueries - )} - {showScheduleEditorModal && fleetQueries && ( - - )} - {showRemoveScheduledQueryModal && ( - - )} - {showShowQueryModal && ( - - )} -
-
- ); -}; - -export default ManageSchedulePage; diff --git a/frontend/pages/schedule/ManageSchedulePage/_styles.scss b/frontend/pages/schedule/ManageSchedulePage/_styles.scss deleted file mode 100644 index f93847b34b..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/_styles.scss +++ /dev/null @@ -1,122 +0,0 @@ -.manage-schedule-page { - &__header-wrap { - display: flex; - align-items: center; - justify-content: space-between; - height: 38px; - } - - &__header { - display: flex; - align-items: center; - - .form-field { - margin-bottom: 0; - } - } - - &__text { - margin-right: $pad-large; - } - - &__title { - font-size: $large; - - .fleeticon { - color: $core-fleet-blue; - margin-right: 15px; - } - - .fleeticon-success-check { - color: $ui-success; - } - - .fleeticon-offline { - color: $ui-error; - } - } - - &__description { - margin: 0; - margin-bottom: $pad-xxlarge; - - h2 { - text-transform: uppercase; - color: $core-fleet-black; - font-weight: $regular; - font-size: $small; - } - - p { - color: $ui-fleet-black-75; - margin: 0; - font-size: $x-small; - font-style: italic; - } - } - - &__action-button-container { - display: flex; - align-items: flex-start; - } - - &__advanced-button { - margin-right: $pad-medium; - } - - .Select.is-open { - .Select-value-label { - color: $core-vibrant-blue !important; - } - } - - .schedule-table { - .data-table-block { - .data-table__table { - thead { - .query_name__header { - width: $col-lg; - } - .interval__header { - width: auto; - } - .actions__header { - width: auto; - } - @media (min-width: $break-lg) { - .interval__header { - width: 0; - } - } - } - tbody { - .query_name__cell { - width: $col-lg; - max-width: 175px; // Truncates at smaller widths - } - .interval__cell { - width: auto; - } - .actions__cell { - width: auto; - } - @media (min-width: $break-lg) { - .interval_cell { - width: 0; - } - } - } - } - } - - .empty-table__container { - max-width: 465px; // Fixes wider font causing orphaned word on all teams empty state - } - } - - .no-team-schedule { - border: 1px solid #e2e4ea; - box-sizing: border-box; - border-radius: 8px; - } -} diff --git a/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/PreviewDataModal.tsx b/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/PreviewDataModal.tsx deleted file mode 100644 index 3156c73288..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/PreviewDataModal.tsx +++ /dev/null @@ -1,61 +0,0 @@ -/* This component is used for creating and editing both global and team scheduled queries */ - -import React from "react"; -import { syntaxHighlight } from "utilities/helpers"; - -import Modal from "components/Modal"; -import Button from "components/buttons/Button"; -import TooltipWrapper from "components/TooltipWrapper"; - -const baseClass = "preview-data-modal"; - -interface IPreviewDataModalProps { - onCancel: () => void; -} - -const PreviewDataModal = ({ - onCancel, -}: IPreviewDataModalProps): JSX.Element => { - const json = { - action: "snapshot", - snapshot: [ - { - remote_address: "0.0.0.0", - remote_port: "0", - cmdline: "/usr/sbin/syslogd", - }, - ], - name: "xxxxxxx", - hostIdentifier: "xxxxxxx", - calendarTime: "xxx xxx x xx:xx:xx xxxx UTC", - unixTime: "xxxxxxxxx", - epoch: "xxxxxxxxx", - counter: "x", - numerics: "x", - }; - - return ( - -
-

- - The data sent to your configured log destination will look similar - to the following JSON: - -

-
-
-        
-
- -
-
-
- ); -}; - -export default PreviewDataModal; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/_styles.scss b/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/_styles.scss deleted file mode 100644 index f965ee2b20..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/_styles.scss +++ /dev/null @@ -1,14 +0,0 @@ -.preview-data-modal { - &__sandbox-info { - margin-top: $pad-medium; - - p { - margin: 0; - margin-bottom: $pad-medium; - } - - p:last-child { - margin-bottom: 0; - } - } -} diff --git a/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/index.ts b/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/index.ts deleted file mode 100644 index 48fca40136..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/PreviewDataModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./PreviewDataModal"; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx b/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx deleted file mode 100644 index fa08aeb3a3..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/RemoveScheduledQueryModal.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from "react"; - -import Modal from "components/Modal"; -import Button from "components/buttons/Button"; - -const baseClass = "remove-scheduled-query-modal"; - -interface IRemoveScheduledQueryModalProps { - isUpdatingScheduledQuery: boolean; - onCancel: () => void; - onSubmit: () => void; -} - -const RemoveScheduledQueryModal = ({ - isUpdatingScheduledQuery, - onCancel, - onSubmit, -}: IRemoveScheduledQueryModalProps): JSX.Element => { - return ( - -
- Are you sure you want to remove the selected queries from the schedule? -
- - -
-
-
- ); -}; - -export default RemoveScheduledQueryModal; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/index.ts b/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/index.ts deleted file mode 100644 index 90280fc7bd..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/RemoveScheduledQueryModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./RemoveScheduledQueryModal"; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx deleted file mode 100644 index f634f803c5..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/ScheduleEditorModal.tsx +++ /dev/null @@ -1,364 +0,0 @@ -/* This component is used for creating and editing both global and team scheduled queries */ - -import React, { useState, useCallback, useContext } from "react"; -import { pull } from "lodash"; -import { AppContext } from "context/app"; - -import { IQuery } from "interfaces/query"; -import { IEditScheduledQuery } from "interfaces/scheduled_query"; - -import Modal from "components/Modal"; -import Button from "components/buttons/Button"; -import RevealButton from "components/buttons/RevealButton"; -import InfoBanner from "components/InfoBanner/InfoBanner"; -// @ts-ignore -import Dropdown from "components/forms/fields/Dropdown"; -// @ts-ignore -import InputField from "components/forms/fields/InputField"; -import CustomLink from "components/CustomLink"; -import { - FREQUENCY_DROPDOWN_OPTIONS, - SCHEDULE_PLATFORM_DROPDOWN_OPTIONS, - LOGGING_TYPE_OPTIONS, - MIN_OSQUERY_VERSION_OPTIONS, -} from "utilities/constants"; - -import PreviewDataModal from "../PreviewDataModal"; - -const baseClass = "schedule-editor-modal"; - -interface IFormData { - interval: number; - name?: string; - shard: number; - query?: string; - query_id?: number; - logging_type: string; - platform: string; - version: string; - team_id?: number; -} - -interface IScheduleEditorModalProps { - allQueries: IQuery[]; - onClose: () => void; - onScheduleSubmit: ( - formData: IFormData, - editQuery: IEditScheduledQuery | undefined - ) => void; - editQuery?: IEditScheduledQuery; - teamId?: number; - togglePreviewDataModal: () => void; - showPreviewDataModal: boolean; - isUpdatingScheduledQuery: boolean; -} -interface INoQueryOption { - id: number; - name: string; -} - -const generateLoggingType = (query: IEditScheduledQuery) => { - if (query.snapshot) { - return "snapshot"; - } - if (query.removed) { - return "differential"; - } - return "differential_ignore_removals"; -}; - -const generateLoggingDestination = (loggingConfig: string): string => { - switch (loggingConfig) { - case "filesystem": - return "the filesystem"; - case "firehose": - return "AWS Kinesis Firehose"; - case "kinesis": - return "AWS Kinesis"; - case "lambda": - return "AWS Lambda"; - case "pubsub": - return "GCP PubSub"; - case "stdout": - return "the standard output stream"; - default: - return loggingConfig; - } -}; - -const ScheduleEditorModal = ({ - onClose, - onScheduleSubmit, - allQueries, - editQuery, - teamId, - togglePreviewDataModal, - showPreviewDataModal, - isUpdatingScheduledQuery, -}: IScheduleEditorModalProps): JSX.Element => { - const { config } = useContext(AppContext); - - const loggingConfig = config?.logging.result.plugin || "unknown"; - - const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); - const [selectedQuery, setSelectedQuery] = useState< - IEditScheduledQuery | INoQueryOption - >(); - const [selectedFrequency, setSelectedFrequency] = useState( - editQuery ? editQuery.interval : 86400 - ); - const [selectedPlatformOptions, setSelectedPlatformOptions] = useState( - editQuery?.platform || "" - ); - const [selectedLoggingType, setSelectedLoggingType] = useState( - editQuery ? generateLoggingType(editQuery) : "snapshot" - ); - const [ - selectedMinOsqueryVersionOptions, - setSelectedMinOsqueryVersionOptions, - ] = useState(editQuery?.version || ""); - const [selectedShard, setSelectedShard] = useState( - editQuery?.shard ? editQuery?.shard.toString() : "" - ); - - const createQueryDropdownOptions = () => { - const queryOptions = allQueries.map((q) => { - return { - value: String(q.id), - label: q.name, - }; - }); - return queryOptions; - }; - - const toggleAdvancedOptions = () => { - setShowAdvancedOptions(!showAdvancedOptions); - }; - - const onChangeSelectQuery = useCallback( - (queryId: string) => { - const queryWithId: IQuery | undefined = allQueries.find( - (query: IQuery) => query.id === parseInt(queryId, 10) - ); - setSelectedQuery(queryWithId); - }, - [allQueries, setSelectedQuery] - ); - - const onChangeSelectFrequency = useCallback( - (value: number) => { - setSelectedFrequency(value); - }, - [setSelectedFrequency] - ); - - const onChangeSelectPlatformOptions = useCallback( - (values: string) => { - const valArray = values.split(","); - - // Remove All if another OS is chosen - // else if Remove OS if All is chosen - if (valArray.indexOf("") === 0 && valArray.length > 1) { - setSelectedPlatformOptions(pull(valArray, "").join(",")); - } else if (valArray.length > 1 && valArray.indexOf("") > -1) { - setSelectedPlatformOptions(""); - } else { - setSelectedPlatformOptions(values); - } - }, - [setSelectedPlatformOptions] - ); - - const onChangeSelectLoggingType = useCallback( - (value: string) => { - setSelectedLoggingType(value); - }, - [setSelectedLoggingType] - ); - - const onChangeMinOsqueryVersionOptions = useCallback( - (value: string) => { - setSelectedMinOsqueryVersionOptions(value); - }, - [setSelectedMinOsqueryVersionOptions] - ); - - const onChangeShard = useCallback( - (value: string) => { - setSelectedShard(value); - }, - [setSelectedShard] - ); - - const onFormSubmit = (): void => { - const query_id = () => { - if (editQuery) { - return editQuery.query_id; - } - return selectedQuery?.id; - }; - - const name = () => { - if (editQuery) { - return editQuery.name; - } - return selectedQuery?.name; - }; - - onScheduleSubmit( - { - shard: parseInt(selectedShard, 10), - interval: selectedFrequency, - query_id: query_id(), - name: name(), - logging_type: selectedLoggingType, - platform: selectedPlatformOptions, - version: selectedMinOsqueryVersionOptions, - team_id: teamId, - }, - editQuery - ); - }; - - if (showPreviewDataModal) { - return ; - } - - return ( - -
-

- Scheduled queries can currently be run on macOS, Windows, and Linux - hosts. Interested in collecting data from your Chromebooks?{" "} - -

- {!editQuery && ( - - )} - - -

- Your configured log destination is {loggingConfig}. -

-

- {loggingConfig === "unknown" - ? "" - : `This means that when this query is run on your hosts, the data will - be sent to ${generateLoggingDestination(loggingConfig)}.`} -

-

- Check out the Fleet documentation on  - - . -

-
-
- - {showAdvancedOptions && ( -
- - - - -
- )} -
-
-
- -
-
- - -
-
- -
- ); -}; - -export default ScheduleEditorModal; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/_styles.scss b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/_styles.scss deleted file mode 100644 index 5682c40509..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/_styles.scss +++ /dev/null @@ -1,26 +0,0 @@ -.schedule-editor-modal { - &__platform-compatibility { - margin-bottom: $pad-large; - } - - &__sandbox-info { - margin-top: $pad-medium; - - p { - margin: 0; - margin-bottom: $pad-medium; - } - - p:last-child { - margin-bottom: 0; - } - } - - &__info-header { - font-weight: $bold; - } - - .Select-value-label { - font-size: $small; - } -} diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/index.ts b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/index.ts deleted file mode 100644 index 2840b8d26c..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleEditorModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./ScheduleEditorModal"; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx deleted file mode 100644 index e5acf57f5d..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx +++ /dev/null @@ -1,224 +0,0 @@ -/** - * Component when there is an error retrieving schedule set up in fleet - */ -import React from "react"; -import { InjectedRouter } from "react-router"; -import paths from "router/paths"; - -import { - IScheduledQuery, - IEditScheduledQuery, -} from "interfaces/scheduled_query"; -import { ITeam } from "interfaces/team"; -import { IEmptyTableProps } from "interfaces/empty_table"; - -import Button from "components/buttons/Button"; -import CustomLink from "components/CustomLink"; -import TableContainer from "components/TableContainer"; -import EmptyTable from "components/EmptyTable"; -import { - generateInheritedQueriesTableHeaders, - generateTableHeaders, - generateDataSet, -} from "./ScheduleTableConfig"; - -const baseClass = "schedule-table"; - -const TAGGED_TEMPLATES = { - hostsByTeamRoute: (teamId: number | undefined | null) => { - return `${teamId ? `/?team_id=${teamId}` : ""}`; - }, -}; -interface IScheduleTableProps { - router: InjectedRouter; // v3 - onRemoveScheduledQueryClick?: (selectedIds: number[]) => void; - onEditScheduledQueryClick?: (selectedQuery: IEditScheduledQuery) => void; - onShowQueryClick?: (selectedQuery: IEditScheduledQuery) => void; - allScheduledQueriesList: IScheduledQuery[]; - toggleScheduleEditorModal?: () => void; - inheritedQueries?: boolean; - isOnGlobalTeam: boolean; - selectedTeamData: ITeam | undefined; - loadingInheritedQueriesTableData: boolean; - loadingTeamQueriesTableData: boolean; -} - -const ScheduleTable = ({ - router, - onRemoveScheduledQueryClick, - onEditScheduledQueryClick, - onShowQueryClick, - allScheduledQueriesList, - toggleScheduleEditorModal, - inheritedQueries, - isOnGlobalTeam, - selectedTeamData, - loadingInheritedQueriesTableData, - loadingTeamQueriesTableData, -}: IScheduleTableProps): JSX.Element => { - const { MANAGE_PACKS, MANAGE_HOSTS } = paths; - - const handleAdvanced = () => router.push(MANAGE_PACKS); - - const emptyState = () => { - const emptySchedule: IEmptyTableProps = { - iconName: "empty-schedule", - header: ( - <> - Schedule queries to run at regular intervals on{" "} - all your hosts - - ), - additionalInfo: ( - <> - Want to learn more?  - - - ), - primaryButton: ( - - ), - }; - - if (selectedTeamData) { - emptySchedule.header = ( - <> - Schedule queries for all hosts assigned to{" "} - - {selectedTeamData.name} - - - ); - } - - /* NOTE: Product decision to remove packs from UI - if (isOnGlobalTeam) { - emptySchedule.info = ( - <>Or go to your osquery packs via the ‘Advanced’ button. - ); - emptySchedule.secondaryButton = ( - - ); - } - */ - return emptySchedule; - }; - - const onActionSelection = ( - action: string, - scheduledQuery: IEditScheduledQuery - ): void => { - switch (action) { - case "edit": - if (onEditScheduledQueryClick) { - onEditScheduledQueryClick(scheduledQuery); - } - break; - case "showQuery": - if (onShowQueryClick) { - onShowQueryClick(scheduledQuery); - } - break; - default: - if (onRemoveScheduledQueryClick) { - onRemoveScheduledQueryClick([scheduledQuery.id]); - } - break; - } - }; - - const tableHeaders = generateTableHeaders(onActionSelection); - const loadingTableData = selectedTeamData?.id - ? loadingTeamQueriesTableData - : loadingInheritedQueriesTableData; - - if (inheritedQueries) { - const inheritedQueriesTableHeaders = generateInheritedQueriesTableHeaders(); - - return ( -
- - EmptyTable({ - iconName: emptyState().iconName, - header: emptyState().header, - info: emptyState().info, - additionalInfo: emptyState().additionalInfo, - primaryButton: emptyState().primaryButton, - secondaryButton: emptyState().secondaryButton, - }) - } - /> -
- ); - } - - return ( -
- - EmptyTable({ - iconName: emptyState().iconName, - header: emptyState().header, - info: emptyState().info, - additionalInfo: emptyState().additionalInfo, - primaryButton: emptyState().primaryButton, - secondaryButton: emptyState().secondaryButton, - }) - } - isClientSidePagination - /> -
- ); -}; - -export default ScheduleTable; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTableConfig.tsx b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTableConfig.tsx deleted file mode 100644 index c4001ab957..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTableConfig.tsx +++ /dev/null @@ -1,272 +0,0 @@ -/* eslint-disable react/prop-types */ -// disable this rule as it was throwing an error in Header and Cell component -// definitions for the selection row for some reason when we dont really need it. -import React from "react"; -import { performanceIndicator, secondsToDhms } from "utilities/helpers"; - -// @ts-ignore -import Checkbox from "components/forms/fields/Checkbox"; -import TextCell from "components/TableContainer/DataTable/TextCell"; -import DropdownCell from "components/TableContainer/DataTable/DropdownCell"; -import PillCell from "components/TableContainer/DataTable/PillCell"; -import { IDropdownOption } from "interfaces/dropdownOption"; -import { - IScheduledQuery, - IEditScheduledQuery, -} from "interfaces/scheduled_query"; -import TooltipWrapper from "components/TooltipWrapper"; - -interface IGetToggleAllRowsSelectedProps { - checked: boolean; - indeterminate: boolean; - title: string; - onChange: () => void; - style: { cursor: string }; -} -interface IHeaderProps { - column: { - title: string; - isSortedDesc: boolean; - }; - getToggleAllRowsSelectedProps: () => IGetToggleAllRowsSelectedProps; - toggleAllRowsSelected: () => void; -} - -interface IRowProps { - row: { - original: IEditScheduledQuery; - getToggleRowSelectedProps: () => IGetToggleAllRowsSelectedProps; - toggleRowSelected: () => void; - }; -} - -interface ICellProps extends IRowProps { - cell: { - value: string | number | boolean; - }; -} - -interface INumberCellProps extends IRowProps { - cell: { - value: number; - }; -} - -interface IPillCellProps extends IRowProps { - cell: { - value: { indicator: string; id: number }; - }; -} - -interface IDropdownCellProps extends IRowProps { - cell: { - value: IDropdownOption[]; - }; -} - -interface IDataColumn { - Header: ((props: IHeaderProps) => JSX.Element) | string; - Cell: - | ((props: ICellProps) => JSX.Element) - | ((props: INumberCellProps) => JSX.Element) - | ((props: IPillCellProps) => JSX.Element) - | ((props: IDropdownCellProps) => JSX.Element); - id?: string; - title?: string; - accessor?: string; - disableHidden?: boolean; - disableSortBy?: boolean; -} -interface IAllScheduledQueryTableData { - name: string; - interval: number; - actions: IDropdownOption[]; - id: number; - type: string; -} - -// NOTE: cellProps come from react-table -// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties -const generateTableHeaders = ( - actionSelectHandler: ( - value: string, - scheduledQuery: IEditScheduledQuery - ) => void -): IDataColumn[] => { - return [ - { - id: "selection", - Header: (cellProps: IHeaderProps): JSX.Element => { - const props = cellProps.getToggleAllRowsSelectedProps(); - const checkboxProps = { - value: props.checked, - indeterminate: props.indeterminate, - onChange: () => cellProps.toggleAllRowsSelected(), - }; - return ; - }, - Cell: (cellProps: ICellProps): JSX.Element => { - const props = cellProps.row.getToggleRowSelectedProps(); - const checkboxProps = { - value: props.checked, - onChange: () => cellProps.row.toggleRowSelected(), - }; - return ; - }, - disableHidden: true, - }, - { - title: "Name", - Header: "Name", - disableSortBy: true, - accessor: "query_name", - Cell: (cellProps: ICellProps): JSX.Element => ( - - ), - }, - { - title: "Frequency", - Header: "Frequency", - disableSortBy: true, - accessor: "interval", - Cell: (cellProps: INumberCellProps): JSX.Element => ( - - ), - }, - { - Header: () => { - return ( -
- - performance impact
- across all hosts where this
- query was scheduled.`} - > - Performance impact -
-
- ); - }, - disableSortBy: true, - accessor: "performance", - Cell: (cellProps: IPillCellProps) => ( - - ), - }, - { - title: "Actions", - Header: "", - disableSortBy: true, - accessor: "actions", - Cell: (cellProps: IDropdownCellProps) => ( - - actionSelectHandler(value, cellProps.row.original) - } - placeholder={"Actions"} - /> - ), - }, - ]; -}; - -const generateInheritedQueriesTableHeaders = (): IDataColumn[] => { - return [ - { - title: "Query", - Header: "Query", - disableSortBy: true, - accessor: "query_name", - Cell: (cellProps: ICellProps): JSX.Element => ( - - ), - }, - { - title: "Frequency", - Header: "Frequency", - disableSortBy: true, - accessor: "interval", - Cell: (cellProps: INumberCellProps): JSX.Element => ( - - ), - }, - { - title: "Performance impact", - Header: "Performance impact", - disableSortBy: true, - accessor: "performance", - Cell: (cellProps: IPillCellProps) => ( - - ), - }, - ]; -}; - -const generateActionDropdownOptions = (): IDropdownOption[] => { - const dropdownOptions = [ - { - label: "Edit", - disabled: false, - value: "edit", - }, - { - label: "Show query", - disabled: false, - value: "showQuery", - }, - { - label: "Remove", - disabled: false, - value: "remove", - }, - ]; - return dropdownOptions; -}; - -const enhanceAllScheduledQueryData = ( - allScheduledQueries: IScheduledQuery[], - teamId: number | undefined -): IAllScheduledQueryTableData[] => { - return allScheduledQueries.map((scheduledQuery: IScheduledQuery) => { - const scheduledQueryPerformance = { - user_time_p50: scheduledQuery.stats?.user_time_p50, - system_time_p50: scheduledQuery.stats?.system_time_p50, - total_executions: scheduledQuery.stats?.total_executions, - }; - return { - name: scheduledQuery.name, - query_name: scheduledQuery.query_name, - interval: scheduledQuery.interval, - actions: generateActionDropdownOptions(), - id: scheduledQuery.id, - query: scheduledQuery.query, - query_id: scheduledQuery.query_id, - snapshot: scheduledQuery.snapshot, - removed: scheduledQuery.removed, - platform: scheduledQuery.platform, - version: scheduledQuery.version, - shard: scheduledQuery.shard, - type: teamId ? "team_scheduled_query" : "global_scheduled_query", - performance: { - indicator: performanceIndicator(scheduledQueryPerformance), - id: scheduledQuery.id, - }, - }; - }); -}; - -const generateDataSet = ( - allScheduledQueries: IScheduledQuery[], - teamId: number | undefined -): IAllScheduledQueryTableData[] => { - return [...enhanceAllScheduledQueryData(allScheduledQueries, teamId)]; -}; - -export { - generateInheritedQueriesTableHeaders, - generateTableHeaders, - generateDataSet, -}; diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/index.ts b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/index.ts deleted file mode 100644 index fb4310e446..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./ScheduleTable"; diff --git a/frontend/pages/schedule/ManageSchedulePage/index.ts b/frontend/pages/schedule/ManageSchedulePage/index.ts deleted file mode 100644 index ab51b9b30f..0000000000 --- a/frontend/pages/schedule/ManageSchedulePage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./ManageSchedulePage"; diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx index 69909599bd..a4ef257c22 100644 --- a/frontend/router/index.tsx +++ b/frontend/router/index.tsx @@ -33,7 +33,6 @@ import ManageSoftwarePage from "pages/software/ManageSoftwarePage"; import ManageQueriesPage from "pages/queries/ManageQueriesPage"; import ManagePacksPage from "pages/packs/ManagePacksPage"; import ManagePoliciesPage from "pages/policies/ManagePoliciesPage"; -import ManageSchedulePage from "pages/schedule/ManageSchedulePage"; import PackComposerPage from "pages/packs/PackComposerPage"; import PolicyPage from "pages/policies/PolicyPage"; import QueryPage from "pages/queries/QueryPage"; @@ -169,7 +168,6 @@ const routes = ( - @@ -204,14 +202,6 @@ const routes = ( - - - - - - - - diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts index 384c0604f6..a4a512a2e2 100644 --- a/frontend/services/entities/queries.ts +++ b/frontend/services/entities/queries.ts @@ -4,6 +4,7 @@ import endpoints from "utilities/endpoints"; import { IQueryFormData } from "interfaces/query"; import { ISelectedTargets } from "interfaces/target"; import { AxiosResponse } from "axios"; +import { buildQueryStringFromParams } from "utilities/url"; // Mock API requests to be used in developing FE for #7765 in parallel with BE development // import { sendRequest } from "services/mock_service/service/service"; @@ -25,16 +26,26 @@ export default { return sendRequest("DELETE", path); }, + bulkDestroy: (ids: number[]) => { + const { QUERIES } = endpoints; + const path = `${QUERIES}/delete`; + return sendRequest("POST", path, { ids }); + }, load: (id: number) => { const { QUERIES } = endpoints; const path = `${QUERIES}/${id}`; return sendRequest("GET", path); }, - loadAll: () => { + loadAll: (teamId?: number) => { const { QUERIES } = endpoints; + const queryString = buildQueryStringFromParams({ team_id: teamId }); + const path = `${QUERIES}`; - return sendRequest("GET", QUERIES); + return sendRequest( + "GET", + queryString ? path.concat(`?${queryString}`) : path + ); }, run: async ({ query,