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,