diff --git a/changes/issue-1894-surface-inherited-queries b/changes/issue-1894-surface-inherited-queries new file mode 100644 index 0000000000..7faae4910e --- /dev/null +++ b/changes/issue-1894-surface-inherited-queries @@ -0,0 +1 @@ +* Fleet premium teams schedules surfaces inherited queries from All teams (global) schedule \ No newline at end of file diff --git a/cypress/integration/all/app/queryflow.spec.ts b/cypress/integration/all/app/queryflow.spec.ts index c91dc70aea..bcc8804123 100644 --- a/cypress/integration/all/app/queryflow.spec.ts +++ b/cypress/integration/all/app/queryflow.spec.ts @@ -9,50 +9,43 @@ describe( cy.login(); }); - // TODO - Fix tests according to improved query experience - MP it("Create, check, edit, and delete a query successfully and create, edit, and delete a global scheduled query successfully", () => { cy.visit("/queries/manage"); - cy.findByRole("button", { name: /create new query/i }).should("exist"); - // cy.findByRole("button", { name: /create new query/i }).click(); + cy.findByRole("button", { name: /create new query/i }).click(); - // cy.findByLabelText(/query name/i) - // .click() - // .type("Query all window crashes"); + // Using class selector because third party element doesn't work with Cypress Testing Selector Library + cy.get(".ace_scroller") + .click({ force: true }) + .type("{selectall}SELECT * FROM windows_crashes;"); - // // Using class selector because third party element doesn't work with Cypress Testing Selector Library - // cy.get(".ace_scroller") - // .click({ force: true }) - // .type("{selectall}{backspace}SELECT * FROM windows_crashes;"); + cy.findByRole("button", { name: /save/i }).click(); - // cy.findByLabelText(/description/i) - // .click() - // .type("See all window crashes"); + cy.findByLabelText(/name/i).click().type("Query all window crashes"); - // cy.findByRole("button", { name: /save/i }).click(); + cy.findByLabelText(/description/i) + .click() + .type("See all window crashes"); - // cy.findByRole("button", { name: /save as new/i }).click(); + cy.findByRole("button", { name: /save query/i }).click(); - // Just refreshes to create new query, needs success alert to user that they created a query - // cy.visit("/queries/manage"); + cy.findByText(/query created/i).should("exist"); + cy.findByText(/back to queries/i).should("exist"); + cy.visit("/queries/manage"); - // cy.findByText(/query all/i).click(); + cy.findByText(/query all/i).click(); - // cy.findByText(/edit & run query/i).should("exist"); + cy.findByText(/run query/i).should("exist"); - // cy.get(".ace_scroller") - // .click({ force: true }) - // .type( - // "{selectall}{backspace}SELECT datetime, username FROM windows_crashes;" - // ); + cy.get(".ace_scroller") + .click({ force: true }) + .type("{selectall}SELECT datetime, username FROM windows_crashes;"); - // cy.findByRole("button", { name: /save/i }).click(); + cy.findByRole("button", { name: /^Save$/ }).click(); - // cy.findByRole("button", { name: /save changes/i }).click(); + cy.findByText(/query updated/i).should("be.visible"); - // cy.findByText(/query updated/i).should("be.visible"); - - // // Test Schedules + // // Start e2e test for schedules // cy.visit("/schedule/manage"); // cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting @@ -85,64 +78,67 @@ describe( // .contains("button", /schedule/i) // .click(); - // cy.visit("/schedule/manage"); + // cy.visit("/schedule/manage"); - // cy.wait(3000); // eslint-disable-line cypress/no-unnecessary-waiting - // cy.findByText(/query all window crashes/i).should("exist"); + // cy.wait(2000); // eslint-disable-line cypress/no-unnecessary-waiting + // cy.findByText(/query all window crashes/i).should("exist"); - // cy.findByText(/actions/i).click(); - // cy.findByText(/edit/i).click(); + // cy.findByText(/actions/i).click(); + // cy.findByText(/edit/i).click(); - // cy.get( - // ".schedule-editor-modal__form-field--frequency > .dropdown__select" - // ).click(); + // cy.get( + // ".schedule-editor-modal__form-field--frequency > .dropdown__select" + // ).click(); - // cy.findByText(/every 6 hours/i).click(); + // cy.findByText(/every 6 hours/i).click(); - // cy.findByText(/show advanced options/i).click(); + // cy.findByText(/show advanced options/i).click(); - // cy.findByText(/ignore removals/i).click(); - // cy.findByText(/snapshot/i).click(); + // cy.findByText(/ignore removals/i).click(); + // cy.findByText(/snapshot/i).click(); - // cy.get(".schedule-editor-modal__form-field--shard > .input-field") - // .click() - // .type("{selectall}{backspace}10"); + // cy.get(".schedule-editor-modal__form-field--shard > .input-field") + // .click() + // .type("{selectall}{backspace}10"); - // cy.get(".schedule-editor-modal__btn-wrap") - // .contains("button", /schedule/i) - // .click(); + // cy.get(".schedule-editor-modal__btn-wrap") + // .contains("button", /schedule/i) + // .click(); - // cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting - // cy.findByText(/actions/i).click(); - // cy.findByText(/remove/i).click(); + // cy.visit("/schedule/manage"); - // cy.get(".remove-scheduled-query-modal__btn-wrap") - // .contains("button", /remove/i) - // .click(); + // cy.wait(2000); // eslint-disable-line cypress/no-unnecessary-waiting + // cy.findByText(/actions/i).click(); + // cy.findByText(/remove/i).click(); - // cy.findByText(/query all window crashes/i).should("not.exist"); - // // End Test Schedules + // cy.get(".remove-scheduled-query-modal__btn-wrap") + // .contains("button", /remove/i) + // .click(); - // cy.visit("/queries/manage"); + // cy.findByText(/query all window crashes/i).should("not.exist"); - // cy.findByText(/query all window crashes/i) - // .parent() - // .parent() - // .within(() => { - // cy.get(".fleet-checkbox__input").check({ force: true }); - // }); + // // End e2e test for schedules - // cy.findByRole("button", { name: /delete/i }).click(); + cy.visit("/queries/manage"); - // // Can't figure out how attach findByRole onto modal button - // // Can't use findByText because delete button under modal - // cy.get(".remove-query-modal") - // .contains("button", /delete/i) - // .click(); + cy.findByText(/query all window crashes/i) + .parent() + .parent() + .within(() => { + cy.get(".fleet-checkbox__input").check({ force: true }); + }); - // cy.findByText(/successfully removed query/i).should("be.visible"); + cy.findByRole("button", { name: /delete/i }).click(); - // cy.findByText(/query all/i).should("not.exist"); + // Can't figure out how attach findByRole onto modal button + // Can't use findByText because delete button under modal + cy.get(".remove-query-modal") + .contains("button", /delete/i) + .click(); + + cy.findByText(/successfully removed query/i).should("be.visible"); + + cy.findByText(/query all/i).should("not.exist"); }); } ); diff --git a/cypress/integration/free/observer.spec.ts b/cypress/integration/free/observer.spec.ts index b020bfd349..253ceb2cf1 100644 --- a/cypress/integration/free/observer.spec.ts +++ b/cypress/integration/free/observer.spec.ts @@ -65,13 +65,11 @@ describe("Free tier - Observer user", () => { cy.findByText(/show sql/i).click(); cy.findByRole("button", { name: /run query/i }).should("exist"); - cy.visit("/queries/manage"); - - cy.findByText(/get authorized/i).should("not.exist"); - // On the Profile page, they should… // See Observer in Role section, and no Team section cy.visit("/profile"); + + cy.wait(2000); // eslint-disable-line cypress/no-unnecessary-waiting cy.findByText(/teams/i).should("not.exist"); cy.findByText("Role") .next() diff --git a/cypress/integration/premium/teamflow.spec.ts b/cypress/integration/premium/teamflow.spec.ts index 0b9e736e68..ba60bb2589 100644 --- a/cypress/integration/premium/teamflow.spec.ts +++ b/cypress/integration/premium/teamflow.spec.ts @@ -6,7 +6,7 @@ describe("Teams flow", () => { }); /* TODO fix and reenable - This test is causing major flake issues due to the dropdown menu + This test is causing major flake issues due to the dropdown menu */ it("Create, edit, and delete a team successfully", () => { cy.visit("/settings/teams"); @@ -44,13 +44,77 @@ describe("Teams flow", () => { cy.contains(/config:/i).should("be.visible"); cy.contains(/options:/i).should("be.visible"); + // Check team in schedules + cy.visit("/queries/manage"); + + cy.findByRole("button", { name: /create new query/i }).click(); + + // Using class selector because third party element doesn't work with Cypress Testing Selector Library + cy.get(".ace_scroller") + .click({ force: true }) + .type("{selectall}SELECT * FROM windows_crashes;"); + + cy.findByRole("button", { name: /save/i }).click(); + + cy.findByLabelText(/name/i).click().type("Query all window crashes"); + + cy.findByLabelText(/description/i) + .click() + .type("See all window crashes"); + + cy.findByRole("button", { name: /save query/i }).click(); + + cy.visit("/schedule/manage"); + + cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting + + cy.findByRole("button", { name: /schedule a query/i }).click(); + + cy.findByText(/select query/i).click(); + + cy.findByText(/query all window crashes/i).click(); + + cy.get( + ".schedule-editor-modal__form-field--frequency > .dropdown__select" + ).click(); + + cy.findByText(/every week/i).click(); + + cy.findByText(/show advanced options/i).click(); + + cy.get( + ".schedule-editor-modal__form-field--logging > .dropdown__select" + ).click(); + + cy.findByText(/ignore removals/i).click(); + + cy.get(".schedule-editor-modal__form-field--shard > .input-field") + .click() + .type("50"); + + cy.get(".schedule-editor-modal__btn-wrap") + .contains("button", /schedule/i) + .click(); + + cy.visit("/schedule/manage"); + + cy.wait(2000); // eslint-disable-line cypress/no-unnecessary-waiting + cy.findByText(/all teams/i).click(); + cy.findByText(/valor/i).click(); + + cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting + cy.findByText(/query all window crashes/i).should("not.exist"); + cy.findByText(/inherited query/i).click(); + cy.findByText(/query all window crashes/i).should("exist"); + + // Edit Team cy.visit("/settings/teams"); - cy.contains("Valor").get(".Select-arrow-zone").click(); - - // need force:true for dropdown - cy.findByText(/edit/i).click({ force: true }); + cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting + cy.findByText(/actions/i).click({ force: true }); + cy.findByText(/edit/i).click({ force: true }); // need force:true for dropdown + cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting cy.findByLabelText(/team name/i) .click() .type("{selectall}{backspace}Mystic"); @@ -66,6 +130,7 @@ describe("Teams flow", () => { cy.findByText(/delete/i).click({ force: true }); + cy.wait(1000); // eslint-disable-line cypress/no-unnecessary-waiting cy.findByRole("button", { name: /delete/i }).click(); cy.findByText(/successfully deleted/i).should("be.visible"); @@ -77,5 +142,4 @@ describe("Teams flow", () => { cy.findByText(/mystic/i).should("not.exist"); }); - */ }); diff --git a/frontend/components/forms/admin/AppConfigForm/AppConfigForm.jsx b/frontend/components/forms/admin/AppConfigForm/AppConfigForm.jsx index e037aca8f3..caca7075c4 100644 --- a/frontend/components/forms/admin/AppConfigForm/AppConfigForm.jsx +++ b/frontend/components/forms/admin/AppConfigForm/AppConfigForm.jsx @@ -11,7 +11,6 @@ import enrollSecretInterface from "interfaces/enroll_secret"; import EnrollSecretTable from "components/config/EnrollSecretTable"; import InputField from "components/forms/fields/InputField"; import OrgLogoIcon from "components/icons/OrgLogoIcon"; -import Slider from "components/forms/fields/Slider"; import validate from "components/forms/admin/AppConfigForm/validate"; import IconToolTip from "components/IconToolTip"; import InfoBanner from "components/InfoBanner/InfoBanner"; diff --git a/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx b/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx index ff881bc279..84dc6cbb20 100644 --- a/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx +++ b/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx @@ -25,6 +25,7 @@ import paths from "router/paths"; import Button from "components/buttons/Button"; // @ts-ignore import Dropdown from "components/forms/fields/Dropdown"; +import IconToolTip from "components/IconToolTip"; import TableDataError from "components/TableDataError"; import ScheduleListWrapper from "./components/ScheduleListWrapper"; import ScheduleEditorModal from "./components/ScheduleEditorModal"; @@ -33,8 +34,10 @@ import RemoveScheduledQueryModal from "./components/RemoveScheduledQueryModal"; const baseClass = "manage-schedule-page"; const renderTable = ( - onRemoveScheduledQueryClick: React.MouseEventHandler, - onEditScheduledQueryClick: React.MouseEventHandler, + onRemoveScheduledQueryClick: (selectIds: number[]) => void, + onEditScheduledQueryClick: ( + selectedQuery: IGlobalScheduledQuery | ITeamScheduledQuery + ) => void, allScheduledQueriesList: IGlobalScheduledQuery[] | ITeamScheduledQuery[], allScheduledQueriesError: { name: string; reason: string }[], toggleScheduleEditorModal: () => void, @@ -55,6 +58,26 @@ const renderTable = ( ); }; +const renderAllTeamsTable = ( + teamId: number, + allTeamsScheduledQueriesList: IGlobalScheduledQuery[], + allTeamsScheduledQueriesError: { name: string; reason: string }[] +): JSX.Element => { + if (Object.keys(allTeamsScheduledQueriesError).length > 0) { + return ; + } + + return ( +
+ +
+ ); +}; + interface ITeamSchedulesPageProps { params: { team_id: string; @@ -139,6 +162,18 @@ const ManageSchedulePage = (props: ITeamSchedulesPageProps): JSX.Element => { const allScheduledQueriesList = Object.values(allScheduledQueries.data); const allScheduledQueriesError = allScheduledQueries.errors; + const allTeamsScheduledQueries = useSelector((state: IRootState) => { + return state.entities.global_scheduled_queries; + }); + + const allTeamsScheduledQueriesList = Object.values( + allTeamsScheduledQueries.data + ); + const allTeamsScheduledQueriesError = allTeamsScheduledQueries.errors; + + const inheritedQueryOrQueries = + allTeamsScheduledQueriesList.length === 1 ? "query" : "queries"; + const allTeams = useSelector((state: IRootState) => state.entities.teams); const allTeamsList = Object.values(allTeams.data); @@ -148,7 +183,7 @@ const ManageSchedulePage = (props: ITeamSchedulesPageProps): JSX.Element => { const teamOptions: ITeamOptions[] = [ { disabled: false, - label: "Global", + label: "All teams", value: "global", }, ]; @@ -163,6 +198,9 @@ const ManageSchedulePage = (props: ITeamSchedulesPageProps): JSX.Element => { return teamOptions; }; + const [showInheritedQueries, setShowInheritedQueries] = useState( + false + ); const [showScheduleEditorModal, setShowScheduleEditorModal] = useState(false); const [ showRemoveScheduledQueryModal, @@ -175,6 +213,10 @@ const ManageSchedulePage = (props: ITeamSchedulesPageProps): JSX.Element => { IGlobalScheduledQuery | ITeamScheduledQuery >(); + const toggleInheritedQueries = () => { + setShowInheritedQueries(!showInheritedQueries); + }; + const toggleScheduleEditorModal = useCallback(() => { setSelectedScheduledQuery(undefined); // create modal renders setShowScheduleEditorModal(!showScheduleEditorModal); @@ -184,12 +226,16 @@ const ManageSchedulePage = (props: ITeamSchedulesPageProps): JSX.Element => { setShowRemoveScheduledQueryModal(!showRemoveScheduledQueryModal); }, [showRemoveScheduledQueryModal, setShowRemoveScheduledQueryModal]); - const onRemoveScheduledQueryClick = (selectedTableQueryIds: any): void => { + const onRemoveScheduledQueryClick = ( + selectedTableQueryIds: number[] + ): void => { toggleRemoveScheduledQueryModal(); setSelectedQueryIds(selectedTableQueryIds); }; - const onEditScheduledQueryClick = (selectedQuery: any): void => { + const onEditScheduledQueryClick = ( + selectedQuery: IGlobalScheduledQuery | ITeamScheduledQuery + ): void => { toggleScheduleEditorModal(); setSelectedScheduledQuery(selectedQuery); // edit modal renders }; @@ -379,6 +425,39 @@ const ManageSchedulePage = (props: ITeamSchedulesPageProps): JSX.Element => { teamId )} + {/* must use ternary for NaN */} + {teamId && allTeamsScheduledQueriesList.length > 0 ? ( + <> + + + +
+

Queries from the “All teams”
schedule run on this team’s hosts.

\ + " + } + /> +
+ + ) : null} + {showInheritedQueries && + renderAllTeamsTable( + teamId, + allTeamsScheduledQueriesList, + allTeamsScheduledQueriesError + )} {showScheduleEditorModal && ( void; + onEditScheduledQueryClick?: ( + selectedQuery: IGlobalScheduledQuery | ITeamScheduledQuery + ) => void; allScheduledQueriesList: IGlobalScheduledQuery[] | ITeamScheduledQuery[]; - toggleScheduleEditorModal: () => void; + toggleScheduleEditorModal?: () => void; teamId: number; + inheritedQueries?: boolean; } interface IRootState { entities: { @@ -47,6 +54,7 @@ const ScheduleListWrapper = (props: IScheduleListWrapperProps): JSX.Element => { toggleScheduleEditorModal, onEditScheduledQueryClick, teamId, + inheritedQueries, } = props; const dispatch = useDispatch(); const { MANAGE_PACKS } = paths; @@ -92,10 +100,14 @@ const ScheduleListWrapper = (props: IScheduleListWrapperProps): JSX.Element => { ): void => { switch (action) { case "edit": - onEditScheduledQueryClick(global_scheduled_query); + if (onEditScheduledQueryClick) { + onEditScheduledQueryClick(global_scheduled_query); + } break; default: - onRemoveScheduledQueryClick([global_scheduled_query.id]); + if (onRemoveScheduledQueryClick) { + onRemoveScheduledQueryClick([global_scheduled_query.id]); + } break; } }; @@ -123,6 +135,33 @@ const ScheduleListWrapper = (props: IScheduleListWrapperProps): JSX.Element => { [dispatch] ); + const loadingInheritedQueriesTableData = useSelector((state: IRootState) => { + return state.entities.global_scheduled_queries.isLoading; + }); + + if (inheritedQueries) { + const inheritedQueriesTableHeaders = generateInheritedQueriesTableHeaders(); + + return ( +
+ +
+ ); + } + return (
{ + 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: ICellProps): JSX.Element => ( + + ), + }, + ]; +}; + const generateActionDropdownOptions = (): IDropdownOption[] => { const dropdownOptions = [ { @@ -162,4 +185,8 @@ const generateDataSet = ( return [...enhanceAllScheduledQueryData(all_scheduled_queries, teamId)]; }; -export { generateTableHeaders, generateDataSet }; +export { + generateInheritedQueriesTableHeaders, + generateTableHeaders, + generateDataSet, +};