Schedules Page: Surface all teams schedule on team schedule page (#2080)

* Render inherited queries table on teams page
* e2e team schedules on e2e premium testing
This commit is contained in:
RachelElysia 2021-09-23 13:10:43 -04:00 committed by GitHub
parent fc00940660
commit 7dee7c56ef
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 356 additions and 93 deletions

View file

@ -0,0 +1 @@
* Fleet premium teams schedules surfaces inherited queries from All teams (global) schedule

View file

@ -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");
});
}
);

View file

@ -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()

View file

@ -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");
});
*/
});

View file

@ -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";

View file

@ -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<HTMLButtonElement>,
onEditScheduledQueryClick: React.MouseEventHandler<HTMLButtonElement>,
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 <TableDataError />;
}
return (
<div className={`${baseClass}__all-teams-table`}>
<ScheduleListWrapper
inheritedQueries
allScheduledQueriesList={allTeamsScheduledQueriesList}
teamId={teamId}
/>
</div>
);
};
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<boolean>(
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
)}
</div>
{/* must use ternary for NaN */}
{teamId && allTeamsScheduledQueriesList.length > 0 ? (
<>
<span>
<Button
variant="unstyled"
className={`${showInheritedQueries ? "upcarat" : "rightcarat"}
${baseClass}__inherited-queries-button`}
onClick={toggleInheritedQueries}
>
{showInheritedQueries
? `Hide ${allTeamsScheduledQueriesList.length} inherited ${inheritedQueryOrQueries}`
: `Show ${allTeamsScheduledQueriesList.length} inherited ${inheritedQueryOrQueries}`}
</Button>
</span>
<div className={`${baseClass}__details`}>
<IconToolTip
isHtml
text={
"\
<center><p>Queries from the All teams<br/>schedule run on this teams hosts.</p></center>\
"
}
/>
</div>
</>
) : null}
{showInheritedQueries &&
renderAllTeamsTable(
teamId,
allTeamsScheduledQueriesList,
allTeamsScheduledQueriesError
)}
{showScheduleEditorModal && (
<ScheduleEditorModal
onCancel={toggleScheduleEditorModal}

View file

@ -174,4 +174,64 @@
}
}
}
&__inherited-queries-button {
margin: $pad-medium 0 0 0;
color: $core-vibrant-blue;
font-weight: $bold;
font-size: $x-small;
}
.rightcarat {
&::before {
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
transform: scale(0.5) rotate(-90deg);
width: 16px;
border-radius: 0px;
padding: 0px;
padding-right: 10px;
margin-top: 5px;
}
}
.upcarat {
&::before {
content: url("../assets/images/icon-chevron-blue-16x16@2x.png");
transform: scale(0.5) rotate(180deg);
width: 16px;
border-radius: 0px;
padding: 0px;
padding-right: 2px;
margin-right: $pad-small;
margin-top: 5px;
position: relative;
top: -4px;
left: 6px;
}
}
&__details {
display: inline-flex;
vertical-align: middle;
margin-left: $pad-small;
margin-top: -20px;
.hint {
color: $core-fleet-black;
&--brand {
color: $core-vibrant-blue;
}
}
}
&__all-teams-table {
.table-container__header {
height: 0;
}
th {
border-right: 1px solid #e2e4ea !important;
}
}
}

View file

@ -13,7 +13,11 @@ import { ITeamScheduledQuery } from "interfaces/team_scheduled_query";
import globalScheduledQueryActions from "redux/nodes/entities/global_scheduled_queries/actions";
import TableContainer from "components/TableContainer";
import { generateTableHeaders, generateDataSet } from "./ScheduleTableConfig";
import {
generateInheritedQueriesTableHeaders,
generateTableHeaders,
generateDataSet,
} from "./ScheduleTableConfig";
// @ts-ignore
import scheduleSvg from "../../../../../../assets/images/schedule.svg";
@ -21,11 +25,14 @@ const baseClass = "schedule-list-wrapper";
const noScheduleClass = "no-schedule";
interface IScheduleListWrapperProps {
onRemoveScheduledQueryClick: any;
onEditScheduledQueryClick: any;
onRemoveScheduledQueryClick?: (selectIds: number[]) => 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 (
<div className={`${baseClass}`}>
<TableContainer
resultsTitle={"queries"}
columns={inheritedQueriesTableHeaders}
data={generateDataSet(allScheduledQueriesList, teamId)}
isLoading={loadingInheritedQueriesTableData}
defaultSortHeader={"query"}
defaultSortDirection={"desc"}
showMarkAllPages={false}
isAllPagesSelected={false}
searchable={false}
disablePagination
disableCount
emptyComponent={NoScheduledQueries}
/>
</div>
);
}
return (
<div className={`${baseClass}`}>
<TableContainer

View file

@ -115,6 +115,29 @@ const generateTableHeaders = (
];
};
const generateInheritedQueriesTableHeaders = (): IDataColumn[] => {
return [
{
title: "Query",
Header: "Query",
disableSortBy: true,
accessor: "query_name",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={cellProps.cell.value} />
),
},
{
title: "Frequency",
Header: "Frequency",
disableSortBy: true,
accessor: "interval",
Cell: (cellProps: ICellProps): JSX.Element => (
<TextCell value={secondsToDhms(cellProps.cell.value)} />
),
},
];
};
const generateActionDropdownOptions = (): IDropdownOption[] => {
const dropdownOptions = [
{
@ -162,4 +185,8 @@ const generateDataSet = (
return [...enhanceAllScheduledQueryData(all_scheduled_queries, teamId)];
};
export { generateTableHeaders, generateDataSet };
export {
generateInheritedQueriesTableHeaders,
generateTableHeaders,
generateDataSet,
};