From ee365ea276e0895aaa5e096cd8bbe46781f9e36c Mon Sep 17 00:00:00 2001
From: RachelElysia <71795832+RachelElysia@users.noreply.github.com>
Date: Mon, 2 Oct 2023 12:26:53 -0700
Subject: [PATCH 01/18] Frontend: Step 1 for query caching (organize FE
directories, build save modals, modify routes/URLs) (#13908)
---
assets/images/phone-home.svg | 1 -
frontend/components/EmptyTable/_styles.scss | 1 -
.../components/LiveQuery/SelectTargets.tsx | 4 +-
.../LiveQuery/TargetsInput/_styles.scss | 3 +
.../TooltipWrapper/TooltipWrapper.tsx | 1 +
.../components/icons/CollectingResults.tsx | 254 ++++++++++++++++++
frontend/components/icons/index.ts | 2 +
.../AwaitingResults/AwaitingResults.tsx | 17 +-
.../queryResults/AwaitingResults/_styles.scss | 15 --
frontend/context/query.tsx | 21 ++
frontend/interfaces/target.ts | 3 +
.../HostDetailsPage/HostDetailsPage.tsx | 12 +-
.../pages/policies/PolicyPage/PolicyPage.tsx | 18 +-
.../pages/policies/PolicyPage/_styles.scss | 32 ---
.../PolicyPage/screens/QueryEditor.tsx | 2 +-
.../QueriesTable/QueriesTableConfig.tsx | 2 +-
frontend/pages/queries/QueryPage/index.ts | 1 -
.../queries/QueryPage/screens/QueryEditor.tsx | 186 -------------
.../pages/queries/QueryPage/screens/test.js | 0
.../QueryDetailsPage/QueryDetailsPage.tsx | 241 +++++++++++++++++
.../details/QueryDetailsPage/_styles.scss | 65 +++++
.../queries/details/QueryDetailsPage/index.ts | 1 +
.../CachedDetails/CachedDetails.tsx | 14 +
.../details/components/CachedDetails/index.ts | 1 +
.../components/NoResults/NoResults.tsx | 116 ++++++++
.../details/components/NoResults/index.ts | 1 +
.../EditQueryPage/EditQueryPage.tsx} | 237 +++++++++-------
.../queries/edit/EditQueryPage/_styles.scss | 54 ++++
.../pages/queries/edit/EditQueryPage/index.ts | 1 +
.../components/QueryForm/QueryForm.tests.tsx | 1 -
.../components/QueryForm/QueryForm.tsx | 46 +++-
.../components/QueryForm/_styles.scss | 5 -
.../components/QueryForm/index.ts | 0
.../components/QueryResults/QueryResults.tsx | 0
.../QueryResults/QueryResultsTableConfig.tsx | 0
.../components/QueryResults/_styles.scss | 0
.../components/QueryResults/index.ts | 0
.../SaveChangesModal/SaveChangesModal.tsx | 53 ++++
.../edit/components/SaveChangesModal/index.ts | 1 +
.../SaveQueryModal/SaveQueryModal.tsx | 214 ++++++++-------
.../components/SaveQueryModal/_styles.scss | 0
.../components/SaveQueryModal/index.ts | 0
.../live/LiveQueryPage/LiveQueryPage.tsx | 213 +++++++++++++++
.../LiveQueryPage}/_styles.scss | 63 +----
.../pages/queries/live/LiveQueryPage/index.ts | 1 +
.../{QueryPage => live}/screens/RunQuery.tsx | 2 +-
frontend/router/index.tsx | 12 +-
frontend/router/paths.ts | 10 +
frontend/services/entities/queries.ts | 4 +-
frontend/utilities/constants.ts | 7 +-
frontend/utilities/endpoints.ts | 2 +-
frontend/utilities/helpers.ts | 7 +
52 files changed, 1391 insertions(+), 556 deletions(-)
delete mode 100644 assets/images/phone-home.svg
create mode 100644 frontend/components/icons/CollectingResults.tsx
delete mode 100644 frontend/pages/queries/QueryPage/index.ts
delete mode 100644 frontend/pages/queries/QueryPage/screens/QueryEditor.tsx
delete mode 100644 frontend/pages/queries/QueryPage/screens/test.js
create mode 100644 frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
create mode 100644 frontend/pages/queries/details/QueryDetailsPage/_styles.scss
create mode 100644 frontend/pages/queries/details/QueryDetailsPage/index.ts
create mode 100644 frontend/pages/queries/details/components/CachedDetails/CachedDetails.tsx
create mode 100644 frontend/pages/queries/details/components/CachedDetails/index.ts
create mode 100644 frontend/pages/queries/details/components/NoResults/NoResults.tsx
create mode 100644 frontend/pages/queries/details/components/NoResults/index.ts
rename frontend/pages/queries/{QueryPage/QueryPage.tsx => edit/EditQueryPage/EditQueryPage.tsx} (54%)
create mode 100644 frontend/pages/queries/edit/EditQueryPage/_styles.scss
create mode 100644 frontend/pages/queries/edit/EditQueryPage/index.ts
rename frontend/pages/queries/{QueryPage => edit}/components/QueryForm/QueryForm.tests.tsx (98%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryForm/QueryForm.tsx (95%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryForm/_styles.scss (98%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryForm/index.ts (100%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryResults/QueryResults.tsx (100%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryResults/QueryResultsTableConfig.tsx (100%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryResults/_styles.scss (100%)
rename frontend/pages/queries/{QueryPage => edit}/components/QueryResults/index.ts (100%)
create mode 100644 frontend/pages/queries/edit/components/SaveChangesModal/SaveChangesModal.tsx
create mode 100644 frontend/pages/queries/edit/components/SaveChangesModal/index.ts
rename frontend/pages/queries/{QueryPage => edit}/components/SaveQueryModal/SaveQueryModal.tsx (51%)
rename frontend/pages/queries/{QueryPage => edit}/components/SaveQueryModal/_styles.scss (100%)
rename frontend/pages/queries/{QueryPage => edit}/components/SaveQueryModal/index.ts (100%)
create mode 100644 frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx
rename frontend/pages/queries/{QueryPage => live/LiveQueryPage}/_styles.scss (69%)
create mode 100644 frontend/pages/queries/live/LiveQueryPage/index.ts
rename frontend/pages/queries/{QueryPage => live}/screens/RunQuery.tsx (98%)
diff --git a/assets/images/phone-home.svg b/assets/images/phone-home.svg
deleted file mode 100644
index a0335a1113..0000000000
--- a/assets/images/phone-home.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/frontend/components/EmptyTable/_styles.scss b/frontend/components/EmptyTable/_styles.scss
index c24d1c9543..15517d8d8f 100644
--- a/frontend/components/EmptyTable/_styles.scss
+++ b/frontend/components/EmptyTable/_styles.scss
@@ -57,7 +57,6 @@
&__container {
align-self: center;
justify-content: center;
- margin: 0;
margin-bottom: 20px;
min-height: 155px;
max-width: none;
diff --git a/frontend/components/LiveQuery/SelectTargets.tsx b/frontend/components/LiveQuery/SelectTargets.tsx
index c65a3e0e60..2052d13e14 100644
--- a/frontend/components/LiveQuery/SelectTargets.tsx
+++ b/frontend/components/LiveQuery/SelectTargets.tsx
@@ -48,7 +48,9 @@ interface ISelectTargetsProps {
targetedTeams: ITeam[];
goToQueryEditor: () => void;
goToRunQuery: () => void;
- setSelectedTargets: React.Dispatch>;
+ setSelectedTargets:
+ | React.Dispatch> // Used for policies page level useState hook
+ | ((value: ITarget[]) => void); // Used for queries app level QueryContext
setTargetedHosts: React.Dispatch>;
setTargetedLabels: React.Dispatch>;
setTargetedTeams: React.Dispatch>;
diff --git a/frontend/components/LiveQuery/TargetsInput/_styles.scss b/frontend/components/LiveQuery/TargetsInput/_styles.scss
index e2f14ce6e1..137525dd04 100644
--- a/frontend/components/LiveQuery/TargetsInput/_styles.scss
+++ b/frontend/components/LiveQuery/TargetsInput/_styles.scss
@@ -79,4 +79,7 @@
overflow: auto;
}
}
+ .input-icon-field__icon {
+ top: 34px; // Override styling to include label header
+ }
}
diff --git a/frontend/components/TooltipWrapper/TooltipWrapper.tsx b/frontend/components/TooltipWrapper/TooltipWrapper.tsx
index 0b48e54b91..43e334495a 100644
--- a/frontend/components/TooltipWrapper/TooltipWrapper.tsx
+++ b/frontend/components/TooltipWrapper/TooltipWrapper.tsx
@@ -6,6 +6,7 @@ import * as DOMPurify from "dompurify";
interface ITooltipWrapperProps {
children: string | JSX.Element;
tipContent: string;
+ /** Default: bottom */
position?: "top" | "bottom";
isDelayed?: boolean;
className?: string;
diff --git a/frontend/components/icons/CollectingResults.tsx b/frontend/components/icons/CollectingResults.tsx
new file mode 100644
index 0000000000..0df9824b3e
--- /dev/null
+++ b/frontend/components/icons/CollectingResults.tsx
@@ -0,0 +1,254 @@
+import React from "react";
+
+const CollectingResults = () => {
+ return (
+
+ );
+};
+
+export default CollectingResults;
diff --git a/frontend/components/icons/index.ts b/frontend/components/icons/index.ts
index cd18850643..36ac93a842 100644
--- a/frontend/components/icons/index.ts
+++ b/frontend/components/icons/index.ts
@@ -4,6 +4,7 @@ import ArrowInternalLink from "./ArrowInternalLink";
import CalendarCheck from "./CalendarCheck";
import Check from "./Check";
import Chevron from "./Chevron";
+import CollectingResults from "./CollectingResults";
import Columns from "./Columns";
import CriticalPolicy from "./CriticalPolicy";
import Disable from "./Disable";
@@ -84,6 +85,7 @@ export const ICON_MAP = {
"calendar-check": CalendarCheck,
chevron: Chevron,
check: Check,
+ "collecting-results": CollectingResults,
columns: Columns,
"critical-policy": CriticalPolicy,
disable: Disable,
diff --git a/frontend/components/queries/queryResults/AwaitingResults/AwaitingResults.tsx b/frontend/components/queries/queryResults/AwaitingResults/AwaitingResults.tsx
index ed2bf4953a..e807a83b6a 100644
--- a/frontend/components/queries/queryResults/AwaitingResults/AwaitingResults.tsx
+++ b/frontend/components/queries/queryResults/AwaitingResults/AwaitingResults.tsx
@@ -1,19 +1,18 @@
import React from "react";
-import PhoneHome from "../../../../../assets/images/phone-home.svg";
+import EmptyTable from "components/EmptyTable/EmptyTable";
const baseClass = "awaiting-results";
const AwaitingResults = () => {
return (
-
-

-
Phoning home...
-
- There are currently no results to your query. Please wait while we talk
- to more hosts.
-
-
+
);
};
diff --git a/frontend/components/queries/queryResults/AwaitingResults/_styles.scss b/frontend/components/queries/queryResults/AwaitingResults/_styles.scss
index 12463a3844..ba2b2dad9b 100644
--- a/frontend/components/queries/queryResults/AwaitingResults/_styles.scss
+++ b/frontend/components/queries/queryResults/AwaitingResults/_styles.scss
@@ -5,19 +5,4 @@
flex-direction: column;
align-items: center;
text-align: center;
-
- img {
- margin-bottom: $pad-medium;
- }
-
- &__title {
- font-size: $small;
- font-weight: $bold;
- margin-bottom: $pad-small;
- }
-
- &__description {
- font-size: $x-small;
- margin: 0;
- }
}
diff --git a/frontend/context/query.tsx b/frontend/context/query.tsx
index 992e152cad..e313a0a156 100644
--- a/frontend/context/query.tsx
+++ b/frontend/context/query.tsx
@@ -6,6 +6,7 @@ import { DEFAULT_QUERY } from "utilities/constants";
import { DEFAULT_OSQUERY_TABLE, IOsQueryTable } from "interfaces/osquery_table";
import { SelectedPlatformString } from "interfaces/platform";
import { QueryLoggingOption } from "interfaces/schedulable_query";
+import { DEFAULT_TARGETS, ITarget } from "interfaces/target";
type Props = {
children: ReactNode;
@@ -22,6 +23,7 @@ type InitialStateType = {
lastEditedQueryPlatforms: SelectedPlatformString;
lastEditedQueryMinOsqueryVersion: string;
lastEditedQueryLoggingType: QueryLoggingOption;
+ selectedQueryTargets: ITarget[];
setLastEditedQueryId: (value: number | null) => void;
setLastEditedQueryName: (value: string) => void;
setLastEditedQueryDescription: (value: string) => void;
@@ -32,6 +34,7 @@ type InitialStateType = {
setLastEditedQueryMinOsqueryVersion: (value: string) => void;
setLastEditedQueryLoggingType: (value: string) => void;
setSelectedOsqueryTable: (tableName: string) => void;
+ setSelectedQueryTargets: (value: ITarget[]) => void;
};
export type IQueryContext = InitialStateType;
@@ -48,6 +51,7 @@ const initialState = {
lastEditedQueryPlatforms: DEFAULT_QUERY.platform,
lastEditedQueryMinOsqueryVersion: DEFAULT_QUERY.min_osquery_version,
lastEditedQueryLoggingType: DEFAULT_QUERY.logging,
+ selectedQueryTargets: DEFAULT_TARGETS,
setLastEditedQueryId: () => null,
setLastEditedQueryName: () => null,
setLastEditedQueryDescription: () => null,
@@ -58,11 +62,13 @@ const initialState = {
setLastEditedQueryMinOsqueryVersion: () => null,
setLastEditedQueryLoggingType: () => null,
setSelectedOsqueryTable: () => null,
+ setSelectedQueryTargets: () => null,
};
const actions = {
SET_SELECTED_OSQUERY_TABLE: "SET_SELECTED_OSQUERY_TABLE",
SET_LAST_EDITED_QUERY_INFO: "SET_LAST_EDITED_QUERY_INFO",
+ SET_SELECTED_QUERY_TARGETS: "SET_SELECTED_QUERY_TARGETS",
} as const;
const reducer = (state: InitialStateType, action: any) => {
@@ -114,6 +120,14 @@ const reducer = (state: InitialStateType, action: any) => {
? state.lastEditedQueryLoggingType
: action.lastEditedQueryLoggingType,
};
+ case actions.SET_SELECTED_QUERY_TARGETS:
+ return {
+ ...state,
+ selectedQueryTargets:
+ typeof action.selectedQueryTargets === "undefined"
+ ? state.selectedQueryTargets
+ : action.selectedQueryTargets,
+ };
default:
return state;
}
@@ -135,6 +149,7 @@ const QueryProvider = ({ children }: Props) => {
lastEditedQueryPlatforms: state.lastEditedQueryPlatforms,
lastEditedQueryMinOsqueryVersion: state.lastEditedQueryMinOsqueryVersion,
lastEditedQueryLoggingType: state.lastEditedQueryLoggingType,
+ selectedQueryTargets: state.selectedQueryTargets,
setLastEditedQueryId: (lastEditedQueryId: number | null) => {
dispatch({
type: actions.SET_LAST_EDITED_QUERY_INFO,
@@ -193,6 +208,12 @@ const QueryProvider = ({ children }: Props) => {
lastEditedQueryLoggingType,
});
},
+ setSelectedQueryTargets: (selectedQueryTargets: ITarget[]) => {
+ dispatch({
+ type: actions.SET_SELECTED_QUERY_TARGETS,
+ selectedQueryTargets,
+ });
+ },
setSelectedOsqueryTable: (tableName: string) => {
dispatch({ type: actions.SET_SELECTED_OSQUERY_TABLE, tableName });
},
diff --git a/frontend/interfaces/target.ts b/frontend/interfaces/target.ts
index 7526719212..873d7907f3 100644
--- a/frontend/interfaces/target.ts
+++ b/frontend/interfaces/target.ts
@@ -49,3 +49,6 @@ export interface IPackTargets {
label_ids: (number | string)[];
team_ids: (number | string)[];
}
+
+// TODO: Also use for testing
+export const DEFAULT_TARGETS: ITarget[] = [];
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
index 0d1a0b6b46..132dd5b502 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
@@ -39,7 +39,11 @@ import MainContent from "components/MainContent";
import InfoBanner from "components/InfoBanner";
import BackLink from "components/BackLink";
-import { normalizeEmptyValues, wrapFleetHelper } from "utilities/helpers";
+import {
+ normalizeEmptyValues,
+ wrapFleetHelper,
+ TAGGED_TEMPLATES,
+} from "utilities/helpers";
import permissions from "utilities/permissions";
import HostSummaryCard from "../cards/HostSummary";
@@ -99,12 +103,6 @@ interface IHostDetailsSubNavItem {
pathname: string;
}
-const TAGGED_TEMPLATES = {
- queryByHostRoute: (hostId: number | undefined | null) => {
- return `${hostId ? `?host_ids=${hostId}` : ""}`;
- },
-};
-
const HostDetailsPage = ({
route,
router,
diff --git a/frontend/pages/policies/PolicyPage/PolicyPage.tsx b/frontend/pages/policies/PolicyPage/PolicyPage.tsx
index ef3fc1eb4c..5d10551229 100644
--- a/frontend/pages/policies/PolicyPage/PolicyPage.tsx
+++ b/frontend/pages/policies/PolicyPage/PolicyPage.tsx
@@ -19,7 +19,7 @@ import globalPoliciesAPI from "services/entities/global_policies";
import teamPoliciesAPI from "services/entities/team_policies";
import hostAPI from "services/entities/hosts";
import statusAPI from "services/entities/status";
-import { QUERIES_PAGE_STEPS } from "utilities/constants";
+import { LIVE_POLICY_STEPS } from "utilities/constants";
import QuerySidePanel from "components/side_panels/QuerySidePanel";
import QueryEditor from "pages/policies/PolicyPage/screens/QueryEditor";
@@ -127,7 +127,7 @@ const PolicyPage = ({
};
}, []);
- const [step, setStep] = useState(QUERIES_PAGE_STEPS[1]);
+ const [step, setStep] = useState(LIVE_POLICY_STEPS[1]);
const [selectedTargets, setSelectedTargets] = useState([]);
const [targetedHosts, setTargetedHosts] = useState([]);
const [targetedLabels, setTargetedLabels] = useState([]);
@@ -260,7 +260,7 @@ const PolicyPage = ({
storedPolicyError,
createPolicy,
onOsqueryTableSelect,
- goToSelectTargets: () => setStep(QUERIES_PAGE_STEPS[2]),
+ goToSelectTargets: () => setStep(LIVE_POLICY_STEPS[2]),
onOpenSchemaSidebar,
renderLiveQueryWarning,
};
@@ -272,8 +272,8 @@ const PolicyPage = ({
targetedLabels,
targetedTeams,
targetsTotalCount,
- goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
- goToRunQuery: () => setStep(QUERIES_PAGE_STEPS[3]),
+ goToQueryEditor: () => setStep(LIVE_POLICY_STEPS[1]),
+ goToRunQuery: () => setStep(LIVE_POLICY_STEPS[3]),
setSelectedTargets,
setTargetedHosts,
setTargetedLabels,
@@ -285,21 +285,21 @@ const PolicyPage = ({
selectedTargets,
storedPolicy,
setSelectedTargets,
- goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
+ goToQueryEditor: () => setStep(LIVE_POLICY_STEPS[1]),
targetsTotalCount,
};
switch (step) {
- case QUERIES_PAGE_STEPS[2]:
+ case LIVE_POLICY_STEPS[2]:
return ;
- case QUERIES_PAGE_STEPS[3]:
+ case LIVE_POLICY_STEPS[3]:
return ;
default:
return ;
}
};
- const isFirstStep = step === QUERIES_PAGE_STEPS[1];
+ const isFirstStep = step === LIVE_POLICY_STEPS[1];
const showSidebar =
isFirstStep &&
isSidebarOpen &&
diff --git a/frontend/pages/policies/PolicyPage/_styles.scss b/frontend/pages/policies/PolicyPage/_styles.scss
index 262b83b515..1eb28ded67 100644
--- a/frontend/pages/policies/PolicyPage/_styles.scss
+++ b/frontend/pages/policies/PolicyPage/_styles.scss
@@ -34,33 +34,6 @@
}
}
- &__observer-query-details {
- padding: 0 2rem;
-
- h1 {
- margin: $pad-large 0;
- font-size: $large;
- }
-
- p {
- margin-bottom: $pad-small;
- }
-
- .sql-button {
- color: $core-vibrant-blue;
- font-weight: $bold;
- font-size: $x-small;
- }
- }
-
- &__query-preview {
- margin-top: 15px;
-
- .fleet-ace__label {
- display: none;
- }
- }
-
.ace_content {
min-height: 500px !important;
}
@@ -177,9 +150,4 @@
margin-bottom: 0;
}
}
- .targets-input {
- .input-icon-field__icon {
- top: 34px; // Override styling to include label header
- }
- }
}
diff --git a/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx b/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx
index 89e46d024a..5e42e673de 100644
--- a/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx
+++ b/frontend/pages/policies/PolicyPage/screens/QueryEditor.tsx
@@ -173,7 +173,7 @@ const QueryEditor = ({
return null;
}
- // Function instead of constant eliminates race condition with filteredSoftwarePath
+ // Function instead of constant eliminates race condition with filteredPoliciesPath
const backToPoliciesPath = () => {
return filteredPoliciesPath || PATHS.MANAGE_POLICIES;
};
diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx
index f24c17af24..10b4611433 100644
--- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx
+++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTableConfig.tsx
@@ -152,7 +152,7 @@ const generateTableHeaders = ({
)}
>
}
- path={PATHS.EDIT_QUERY(
+ path={PATHS.QUERY(
cellProps.row.original.id,
cellProps.row.original.team_id ?? undefined
)}
diff --git a/frontend/pages/queries/QueryPage/index.ts b/frontend/pages/queries/QueryPage/index.ts
deleted file mode 100644
index 8d00fa0475..0000000000
--- a/frontend/pages/queries/QueryPage/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { default } from "./QueryPage";
diff --git a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx b/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx
deleted file mode 100644
index 7b94956efd..0000000000
--- a/frontend/pages/queries/QueryPage/screens/QueryEditor.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import React, { useContext, useEffect, useState } from "react";
-
-import { InjectedRouter } from "react-router/lib/Router";
-import { UseMutateAsyncFunction } from "react-query";
-
-import queryAPI from "services/entities/queries";
-import { AppContext } from "context/app";
-import { QueryContext } from "context/query";
-import { NotificationContext } from "context/notification";
-import {
- ICreateQueryRequestBody,
- ISchedulableQuery,
-} from "interfaces/schedulable_query";
-import PATHS from "router/paths";
-import debounce from "utilities/debounce";
-import deepDifference from "utilities/deep_difference";
-
-import BackLink from "components/BackLink";
-import QueryForm from "pages/queries/QueryPage/components/QueryForm";
-
-interface IQueryEditorProps {
- router: InjectedRouter;
- baseClass: string;
- queryIdForEdit: number | null;
- teamNameForQuery?: string;
- apiTeamIdForQuery?: number;
- storedQuery: ISchedulableQuery | undefined;
- storedQueryError: Error | null;
- showOpenSchemaActionText: boolean;
- isStoredQueryLoading: boolean;
- onOsqueryTableSelect: (tableName: string) => void;
- goToSelectTargets: () => void;
- onOpenSchemaSidebar: () => void;
- renderLiveQueryWarning: () => JSX.Element | null;
-}
-
-const QueryEditor = ({
- router,
- baseClass,
- queryIdForEdit,
- teamNameForQuery,
- apiTeamIdForQuery,
- storedQuery,
- storedQueryError,
- showOpenSchemaActionText,
- isStoredQueryLoading,
- onOsqueryTableSelect,
- goToSelectTargets,
- onOpenSchemaSidebar,
- renderLiveQueryWarning,
-}: IQueryEditorProps): JSX.Element | null => {
- const { currentUser, filteredQueriesPath } = useContext(AppContext);
- const { renderFlash } = useContext(NotificationContext);
-
- // Note: The QueryContext values should always be used for any mutable query data such as query name
- // The storedQuery prop should only be used to access immutable metadata such as author id
- const {
- lastEditedQueryName,
- lastEditedQueryDescription,
- lastEditedQueryBody,
- lastEditedQueryObserverCanRun,
- lastEditedQueryFrequency,
- lastEditedQueryLoggingType,
- lastEditedQueryPlatforms,
- lastEditedQueryMinOsqueryVersion,
- } = useContext(QueryContext);
-
- const [isQuerySaving, setIsQuerySaving] = useState(false);
- const [isQueryUpdating, setIsQueryUpdating] = useState(false);
-
- useEffect(() => {
- if (storedQueryError) {
- renderFlash(
- "error",
- "Something went wrong retrieving your query. Please try again."
- );
- }
- }, []);
-
- const [backendValidators, setBackendValidators] = useState<{
- [key: string]: string;
- }>({});
-
- const saveQuery = debounce(async (formData: ICreateQueryRequestBody) => {
- setIsQuerySaving(true);
- try {
- const { query } = await queryAPI.create(formData);
- router.push(PATHS.EDIT_QUERY(query.id));
- renderFlash("success", "Query created!");
- setBackendValidators({});
- } catch (createError: any) {
- if (createError.data.errors[0].reason.includes("already exists")) {
- const teamErrorText =
- teamNameForQuery && apiTeamIdForQuery !== 0
- ? `the ${teamNameForQuery} team`
- : "all teams";
- setBackendValidators({
- name: `A query with that name already exists for ${teamErrorText}.`,
- });
- } else {
- renderFlash(
- "error",
- "Something went wrong creating your query. Please try again."
- );
- setBackendValidators({});
- }
- } finally {
- setIsQuerySaving(false);
- }
- });
-
- const onUpdateQuery = async (formData: ICreateQueryRequestBody) => {
- if (!queryIdForEdit) {
- return false;
- }
-
- setIsQueryUpdating(true);
-
- const updatedQuery = deepDifference(formData, {
- lastEditedQueryName,
- lastEditedQueryDescription,
- lastEditedQueryBody,
- lastEditedQueryObserverCanRun,
- lastEditedQueryFrequency,
- lastEditedQueryPlatforms,
- lastEditedQueryLoggingType,
- lastEditedQueryMinOsqueryVersion,
- });
-
- try {
- await queryAPI.update(queryIdForEdit, updatedQuery);
- renderFlash("success", "Query updated!");
- } catch (updateError: any) {
- console.error(updateError);
- if (updateError.data.errors[0].reason.includes("Duplicate")) {
- renderFlash("error", "A query with this name already exists.");
- } else {
- renderFlash(
- "error",
- "Something went wrong updating your query. Please try again."
- );
- }
- }
-
- setIsQueryUpdating(false);
-
- return false;
- };
-
- if (!currentUser) {
- return null;
- }
-
- // Function instead of constant eliminates race condition with filteredSoftwarePath
- const backToQueriesPath = () => {
- return filteredQueriesPath || PATHS.MANAGE_QUERIES;
- };
-
- return (
-
- );
-};
-
-export default QueryEditor;
diff --git a/frontend/pages/queries/QueryPage/screens/test.js b/frontend/pages/queries/QueryPage/screens/test.js
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
new file mode 100644
index 0000000000..be52bd6ad3
--- /dev/null
+++ b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
@@ -0,0 +1,241 @@
+import React, { useContext, useEffect } from "react";
+import { useQuery } from "react-query";
+import { InjectedRouter, Params } from "react-router/lib/Router";
+import { useErrorHandler } from "react-error-boundary";
+
+import PATHS from "router/paths";
+import { AppContext } from "context/app";
+import { QueryContext } from "context/query";
+import useTeamIdParam from "hooks/useTeamIdParam";
+
+import {
+ IGetQueryResponse,
+ ISchedulableQuery,
+} from "interfaces/schedulable_query";
+
+import queryAPI from "services/entities/queries";
+
+import Spinner from "components/Spinner/Spinner";
+import Button from "components/buttons/Button";
+import BackLink from "components/BackLink";
+import MainContent from "components/MainContent";
+import TooltipWrapper from "components/TooltipWrapper/TooltipWrapper";
+import QueryAutomationsStatusIndicator from "pages/queries/ManageQueriesPage/components/QueryAutomationsStatusIndicator/QueryAutomationsStatusIndicator";
+import DataError from "components/DataError/DataError";
+import LogDestinationIndicator from "components/LogDestinationIndicator/LogDestinationIndicator";
+import CachedDetails from "../components/CachedDetails/CachedDetails";
+import NoResults from "../components/NoResults/NoResults";
+
+interface IQueryDetailsPageProps {
+ router: InjectedRouter; // v3
+ params: Params;
+ location: {
+ pathname: string;
+ query: { team_id?: string };
+ search: string;
+ };
+}
+
+const baseClass = "query-details-page";
+
+const QueryDetailsPage = ({
+ router,
+ params: { id: paramsQueryId },
+ location,
+}: IQueryDetailsPageProps): JSX.Element => {
+ const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
+
+ const {
+ currentTeamName: teamNameForQuery,
+ teamIdForApi: apiTeamIdForQuery,
+ } = useTeamIdParam({
+ location,
+ router,
+ includeAllTeams: true,
+ includeNoTeam: false,
+ });
+
+ const handlePageError = useErrorHandler();
+ const {
+ isGlobalAdmin,
+ isGlobalMaintainer,
+ isAnyTeamMaintainerOrTeamAdmin,
+ isObserverPlus,
+ isAnyTeamObserverPlus,
+ config,
+ filteredQueriesPath,
+ } = useContext(AppContext);
+ const {
+ lastEditedQueryName,
+ lastEditedQueryDescription,
+ lastEditedQueryObserverCanRun,
+ setLastEditedQueryId,
+ setLastEditedQueryName,
+ setLastEditedQueryDescription,
+ setLastEditedQueryBody,
+ setLastEditedQueryObserverCanRun,
+ setLastEditedQueryFrequency,
+ setLastEditedQueryLoggingType,
+ setLastEditedQueryMinOsqueryVersion,
+ setLastEditedQueryPlatforms,
+ } = useContext(QueryContext);
+
+ // Title that shows up on browser tabs (e.g., Query details | Discover TLS certificates | Fleet for osquery)
+ document.title = `Query details | ${lastEditedQueryName} | Fleet for osquery`;
+
+ // disabled on page load so we can control the number of renders
+ // else it will re-populate the context on occasion
+ const {
+ isLoading: isStoredQueryLoading,
+ data: storedQuery,
+ error: storedQueryError,
+ } = useQuery(
+ ["query", queryId],
+ () => queryAPI.load(queryId as number),
+ {
+ enabled: !!queryId,
+ refetchOnWindowFocus: false,
+ select: (data) => data.query,
+ onSuccess: (returnedQuery) => {
+ setLastEditedQueryId(returnedQuery.id);
+ setLastEditedQueryName(returnedQuery.name);
+ setLastEditedQueryDescription(returnedQuery.description);
+ setLastEditedQueryBody(returnedQuery.query);
+ setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run);
+ setLastEditedQueryFrequency(returnedQuery.interval);
+ setLastEditedQueryPlatforms(returnedQuery.platform);
+ setLastEditedQueryLoggingType(returnedQuery.logging);
+ setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version);
+ },
+ onError: (error) => handlePageError(error),
+ }
+ );
+
+ const isLoading = isStoredQueryLoading; // TODO: Add || isCachedResultsLoading for new API response
+ const isApiError = storedQueryError || true; // TODO: Add || isCachedResultsError for new API response
+
+ const renderHeader = () => {
+ const canEditQuery =
+ isGlobalAdmin || isGlobalMaintainer || isAnyTeamMaintainerOrTeamAdmin;
+
+ // Function instead of constant eliminates race condition with filteredQueriesPath
+ const backToQueriesPath = () => {
+ return filteredQueriesPath || PATHS.MANAGE_QUERIES;
+ };
+
+ return (
+ <>
+
+
+
+ {!isLoading && !isApiError && (
+
+
+
+ {lastEditedQueryName}
+
+
+ {lastEditedQueryDescription}
+
+
+
+
+ {(lastEditedQueryObserverCanRun ||
+ isObserverPlus ||
+ isAnyTeamObserverPlus ||
+ canEditQuery) && (
+
+
+
+ )}
+
+
+ )}
+ {!isLoading && !isApiError && (
+
+
+
+ Automations:
+
+
+
+
+ Log destination:{" "}
+
+
+
+ )}
+ >
+ );
+ };
+
+ const renderReport = () => {
+ const disabledCachingGlobally = true; // TODO: Update accordingly to config?.server_settings.query_reports_disabled
+ const discardDataEnabled = true; // TODO: Update accordingly to storedQuery?.discard_data
+ const loggingSnapshot = storedQuery?.logging === "snapshot";
+ const disabledCaching =
+ disabledCachingGlobally || discardDataEnabled || !loggingSnapshot;
+ const emptyCache = true; // TODO: Update with API response
+ const errorsOnly = true; // TODO: Update with API response
+
+ // Loading state
+ if (isLoading) {
+ return ;
+ }
+
+ // Error state
+ if (isApiError) {
+ return ;
+ }
+
+ // Empty state with varying messages explaining why there's no results
+ if (emptyCache) {
+ return (
+
+ );
+ }
+ return ; // TODO: Everything related to new APIs including surfacing errorsOnly
+ };
+
+ return (
+
+
+ {renderHeader()}
+ {renderReport()}
+
+
+ );
+};
+
+export default QueryDetailsPage;
diff --git a/frontend/pages/queries/details/QueryDetailsPage/_styles.scss b/frontend/pages/queries/details/QueryDetailsPage/_styles.scss
new file mode 100644
index 0000000000..9b0a5a0a25
--- /dev/null
+++ b/frontend/pages/queries/details/QueryDetailsPage/_styles.scss
@@ -0,0 +1,65 @@
+.query-details-page {
+ &__title-bar {
+ display: flex;
+ justify-content: space-between;
+ margin-top: $pad-large;
+
+ .name-description {
+ display: flex;
+ flex-direction: column;
+ gap: $pad-small;
+ }
+ }
+
+ &__action-button-container {
+ display: flex;
+ justify-content: flex-end;
+ min-width: 266px;
+ gap: $pad-medium;
+ }
+
+ &__query-name {
+ margin-top: 0;
+ font-size: $large;
+ }
+
+ &__query-description {
+ margin-top: 0;
+ margin-bottom: $pad-small;
+ font-size: $x-small;
+ }
+
+ &__settings {
+ display: flex;
+ gap: $pad-large;
+ font-size: $x-small;
+ }
+
+ &__automations,
+ &__log-destination {
+ display: flex;
+ gap: $pad-small;
+ }
+
+ .empty-table__inner {
+ .component__tooltip-wrapper__tip-text {
+ text-align: left;
+ width: 320px;
+ }
+
+ ul {
+ color: $core-white;
+
+ li {
+ &::before {
+ content: "•";
+ color: $core-white;
+ }
+ }
+ }
+ }
+
+ .data-error {
+ padding-top: $pad-xxxlarge;
+ }
+}
diff --git a/frontend/pages/queries/details/QueryDetailsPage/index.ts b/frontend/pages/queries/details/QueryDetailsPage/index.ts
new file mode 100644
index 0000000000..9bb526e7b5
--- /dev/null
+++ b/frontend/pages/queries/details/QueryDetailsPage/index.ts
@@ -0,0 +1 @@
+export { default } from "./QueryDetailsPage";
diff --git a/frontend/pages/queries/details/components/CachedDetails/CachedDetails.tsx b/frontend/pages/queries/details/components/CachedDetails/CachedDetails.tsx
new file mode 100644
index 0000000000..ab5255e3f0
--- /dev/null
+++ b/frontend/pages/queries/details/components/CachedDetails/CachedDetails.tsx
@@ -0,0 +1,14 @@
+import React from "react";
+
+// TODO: This whole section
+// interface ICachedDetailsProps {
+//
+// }
+
+const baseClass = "cached-details";
+
+const CachedDetails = (): JSX.Element => {
+ return TODO
;
+};
+
+export default CachedDetails;
diff --git a/frontend/pages/queries/details/components/CachedDetails/index.ts b/frontend/pages/queries/details/components/CachedDetails/index.ts
new file mode 100644
index 0000000000..b50b73552b
--- /dev/null
+++ b/frontend/pages/queries/details/components/CachedDetails/index.ts
@@ -0,0 +1 @@
+export { default } from "./CachedDetails";
diff --git a/frontend/pages/queries/details/components/NoResults/NoResults.tsx b/frontend/pages/queries/details/components/NoResults/NoResults.tsx
new file mode 100644
index 0000000000..7897facb23
--- /dev/null
+++ b/frontend/pages/queries/details/components/NoResults/NoResults.tsx
@@ -0,0 +1,116 @@
+import React from "react";
+
+import differenceInSeconds from "date-fns/differenceInSeconds";
+import formatDistance from "date-fns/formatDistance";
+import add from "date-fns/add";
+
+import TooltipWrapper from "components/TooltipWrapper/TooltipWrapper";
+import EmptyTable from "components/EmptyTable/EmptyTable";
+
+interface INoResultsProps {
+ queryInterval?: number;
+ queryUpdatedAt?: string;
+ disabledCaching: boolean;
+ disabledCachingGlobally: boolean;
+ discardDataEnabled: boolean;
+ loggingSnapshot: boolean;
+ errorsOnly: boolean;
+}
+
+const baseClass = "no-results";
+
+const NoResults = ({
+ queryInterval,
+ queryUpdatedAt,
+ disabledCaching,
+ disabledCachingGlobally,
+ discardDataEnabled,
+ loggingSnapshot,
+ errorsOnly,
+}: INoResultsProps): JSX.Element => {
+ // Returns how many seconds it takes to expect a cached update
+ const secondsCheckbackTime = () => {
+ const secondsSinceUpdate = queryUpdatedAt
+ ? differenceInSeconds(new Date(), new Date(queryUpdatedAt))
+ : 0;
+ const secondsUpdateWaittime = (queryInterval || 0) + 60;
+ return secondsUpdateWaittime - secondsSinceUpdate;
+ };
+
+ // Update status of collecting cached results
+ const collectingResults = secondsCheckbackTime() > 0;
+
+ // Converts seconds takes to update to human readable format
+ const readableCheckbackTime = formatDistance(
+ add(new Date(), { seconds: secondsCheckbackTime() }),
+ new Date()
+ );
+
+ // Collecting results state
+ if (collectingResults) {
+ const collectingResultsInfo = () =>
+ `Fleet is collecting query results. Check back in about ${readableCheckbackTime}.`;
+
+ return (
+
+ );
+ }
+
+ const noResultsInfo = () => {
+ if (!queryInterval) {
+ return (
+ <>
+ This query does not collect data on a schedule. Add a{" "}
+ frequency or run this as a live query to see results.
+ >
+ );
+ }
+ if (disabledCaching) {
+ const tipContent = () => {
+ if (disabledCachingGlobally) {
+ return "The following setting prevents saving this query's results in Fleet:- Query reports are globally disabled in organization settings.
";
+ }
+ if (discardDataEnabled) {
+ return "The following setting prevents saving this query's results in Fleet:- This query has Discard data enabled.
";
+ }
+ if (!loggingSnapshot) {
+ return "The following setting prevents saving this query's results in Fleet:- The logging setting for this query is not Snapshot.
";
+ }
+ return "Unknown";
+ };
+ return (
+ <>
+ Results from this query are{" "}
+
+ not reported in Fleet
+
+ .
+ >
+ );
+ }
+ if (errorsOnly) {
+ return (
+ <>
+ This query had trouble collecting data on some hosts. Check out the{" "}
+ Errors tab to see why.
+ >
+ );
+ }
+ return "This query has returned no data so far.";
+ };
+
+ return (
+
+ );
+};
+
+export default NoResults;
diff --git a/frontend/pages/queries/details/components/NoResults/index.ts b/frontend/pages/queries/details/components/NoResults/index.ts
new file mode 100644
index 0000000000..04bef19e77
--- /dev/null
+++ b/frontend/pages/queries/details/components/NoResults/index.ts
@@ -0,0 +1 @@
+export { default } from "./NoResults";
diff --git a/frontend/pages/queries/QueryPage/QueryPage.tsx b/frontend/pages/queries/edit/EditQueryPage/EditQueryPage.tsx
similarity index 54%
rename from frontend/pages/queries/QueryPage/QueryPage.tsx
rename to frontend/pages/queries/edit/EditQueryPage/EditQueryPage.tsx
index ca93efe3d4..e9aa57dce9 100644
--- a/frontend/pages/queries/QueryPage/QueryPage.tsx
+++ b/frontend/pages/queries/edit/EditQueryPage/EditQueryPage.tsx
@@ -1,34 +1,36 @@
-import React, { useState, useEffect, useContext, useCallback } from "react";
-import { useQuery, useMutation } from "react-query";
+import React, { useState, useEffect, useContext } from "react";
+import { useQuery } from "react-query";
import { useErrorHandler } from "react-error-boundary";
import { InjectedRouter, Params } from "react-router/lib/Router";
import { AppContext } from "context/app";
import { QueryContext } from "context/query";
-import { QUERIES_PAGE_STEPS, DEFAULT_QUERY } from "utilities/constants";
+import { DEFAULT_QUERY } from "utilities/constants";
import queryAPI from "services/entities/queries";
-import hostAPI from "services/entities/hosts";
import statusAPI from "services/entities/status";
-import { IHost, IHostResponse } from "interfaces/host";
-import { ILabel } from "interfaces/label";
-import { ITeam } from "interfaces/team";
import {
IGetQueryResponse,
+ ICreateQueryRequestBody,
ISchedulableQuery,
} from "interfaces/schedulable_query";
-import { ITarget } from "interfaces/target";
import QuerySidePanel from "components/side_panels/QuerySidePanel";
import MainContent from "components/MainContent";
import SidePanelContent from "components/SidePanelContent";
-import SelectTargets from "components/LiveQuery/SelectTargets";
import CustomLink from "components/CustomLink";
-import QueryEditor from "pages/queries/QueryPage/screens/QueryEditor";
-import RunQuery from "pages/queries/QueryPage/screens/RunQuery";
import useTeamIdParam from "hooks/useTeamIdParam";
-interface IQueryPageProps {
+import { NotificationContext } from "context/notification";
+
+import PATHS from "router/paths";
+import debounce from "utilities/debounce";
+import deepDifference from "utilities/deep_difference";
+
+import BackLink from "components/BackLink";
+import QueryForm from "pages/queries/edit/components/QueryForm";
+
+interface IEditQueryPageProps {
router: InjectedRouter;
params: Params;
location: {
@@ -38,13 +40,13 @@ interface IQueryPageProps {
};
}
-const baseClass = "query-page";
+const baseClass = "edit-query-page";
-const QueryPage = ({
+const EditQueryPage = ({
router,
params: { id: paramsQueryId },
location,
-}: IQueryPageProps): JSX.Element => {
+}: IEditQueryPageProps): JSX.Element => {
const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
const {
currentTeamName: teamNameForQuery,
@@ -67,6 +69,15 @@ const QueryPage = ({
const {
selectedOsqueryTable,
setSelectedOsqueryTable,
+ lastEditedQueryName,
+ lastEditedQueryDescription,
+ lastEditedQueryBody,
+ lastEditedQueryObserverCanRun,
+ lastEditedQueryFrequency,
+ lastEditedQueryPlatforms,
+ lastEditedQueryLoggingType,
+ lastEditedQueryMinOsqueryVersion,
+ selectedQueryTargets,
setLastEditedQueryId,
setLastEditedQueryName,
setLastEditedQueryDescription,
@@ -76,15 +87,14 @@ const QueryPage = ({
setLastEditedQueryLoggingType,
setLastEditedQueryMinOsqueryVersion,
setLastEditedQueryPlatforms,
+ // setSelectedQueryTargets,
} = useContext(QueryContext);
+ const { currentUser } = useContext(AppContext);
+ const { renderFlash } = useContext(NotificationContext);
+
+ // const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false);
+ // const [targetedHosts, setTargetedHosts] = useState([]);
- const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false);
- const [step, setStep] = useState(QUERIES_PAGE_STEPS[1]);
- const [selectedTargets, setSelectedTargets] = useState([]);
- const [targetedHosts, setTargetedHosts] = useState([]);
- const [targetedLabels, setTargetedLabels] = useState([]);
- const [targetedTeams, setTargetedTeams] = useState([]);
- const [targetsTotalCount, setTargetsTotalCount] = useState(0);
const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [showOpenSchemaActionText, setShowOpenSchemaActionText] = useState(
@@ -119,29 +129,6 @@ const QueryPage = ({
}
);
- useQuery(
- "hostFromURL",
- () =>
- hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)),
- {
- enabled: !!location.query.host_ids && !queryParamHostsAdded,
- select: (data: IHostResponse) => data.host,
- onSuccess: (host) => {
- setTargetedHosts((prevHosts) =>
- prevHosts.filter((h) => h.id !== host.id).concat(host)
- );
- const targets = selectedTargets;
- host.target_type = "hosts";
- targets.push(host);
- setSelectedTargets([...targets]);
- if (!queryParamHostsAdded) {
- setQueryParamHostsAdded(true);
- }
- router.replace(location.pathname);
- },
- }
- );
-
const detectIsFleetQueryRunnable = () => {
statusAPI.live_query().catch(() => {
setIsLiveQueryRunnable(false);
@@ -163,16 +150,88 @@ const QueryPage = ({
}
}, [queryId]);
+ const [isQuerySaving, setIsQuerySaving] = useState(false);
+ const [isQueryUpdating, setIsQueryUpdating] = useState(false);
+ const [backendValidators, setBackendValidators] = useState<{
+ [key: string]: string;
+ }>({});
+
// Updates title that shows up on browser tabs
useEffect(() => {
// e.g., Query details | Discover TLS certificates | Fleet for osquery
- document.title = `Query details | ${storedQuery?.name} | Fleet for osquery`;
+ document.title = `Edit query | ${storedQuery?.name} | Fleet for osquery`;
}, [location.pathname, storedQuery?.name]);
useEffect(() => {
setShowOpenSchemaActionText(!isSidebarOpen);
}, [isSidebarOpen]);
+ const saveQuery = debounce(async (formData: ICreateQueryRequestBody) => {
+ setIsQuerySaving(true);
+ try {
+ const { query } = await queryAPI.create(formData);
+ router.push(PATHS.EDIT_QUERY(query.id));
+ renderFlash("success", "Query created!");
+ setBackendValidators({});
+ } catch (createError: any) {
+ if (createError.data.errors[0].reason.includes("already exists")) {
+ const teamErrorText =
+ teamNameForQuery && apiTeamIdForQuery !== 0
+ ? `the ${teamNameForQuery} team`
+ : "all teams";
+ setBackendValidators({
+ name: `A query with that name already exists for ${teamErrorText}.`,
+ });
+ } else {
+ renderFlash(
+ "error",
+ "Something went wrong creating your query. Please try again."
+ );
+ setBackendValidators({});
+ }
+ } finally {
+ setIsQuerySaving(false);
+ }
+ });
+
+ const onUpdateQuery = async (formData: ICreateQueryRequestBody) => {
+ if (!queryId) {
+ return false;
+ }
+
+ setIsQueryUpdating(true);
+
+ const updatedQuery = deepDifference(formData, {
+ lastEditedQueryName,
+ lastEditedQueryDescription,
+ lastEditedQueryBody,
+ lastEditedQueryObserverCanRun,
+ lastEditedQueryFrequency,
+ lastEditedQueryPlatforms,
+ lastEditedQueryLoggingType,
+ lastEditedQueryMinOsqueryVersion,
+ });
+
+ try {
+ await queryAPI.update(queryId, updatedQuery);
+ renderFlash("success", "Query updated!");
+ } catch (updateError: any) {
+ console.error(updateError);
+ if (updateError.data.errors[0].reason.includes("Duplicate")) {
+ renderFlash("error", "A query with this name already exists.");
+ } else {
+ renderFlash(
+ "error",
+ "Something went wrong updating your query. Please try again."
+ );
+ }
+ }
+
+ setIsQueryUpdating(false);
+
+ return false;
+ };
+
const onOsqueryTableSelect = (tableName: string) => {
setSelectedOsqueryTable(tableName);
};
@@ -207,64 +266,12 @@ const QueryPage = ({
);
};
- const goToQueryEditor = useCallback(() => setStep(QUERIES_PAGE_STEPS[1]), []);
-
- const renderScreen = () => {
- const step1Props = {
- router,
- baseClass,
- queryIdForEdit: queryId,
- teamNameForQuery,
- apiTeamIdForQuery,
- showOpenSchemaActionText,
- storedQuery,
- isStoredQueryLoading,
- storedQueryError,
- onOsqueryTableSelect,
- goToSelectTargets: () => setStep(QUERIES_PAGE_STEPS[2]),
- onOpenSchemaSidebar,
- renderLiveQueryWarning,
- };
-
- const step2Props = {
- baseClass,
- queryId,
- selectedTargets,
- targetedHosts,
- targetedLabels,
- targetedTeams,
- targetsTotalCount,
- goToQueryEditor: () => setStep(QUERIES_PAGE_STEPS[1]),
- goToRunQuery: () => setStep(QUERIES_PAGE_STEPS[3]),
- setSelectedTargets,
- setTargetedHosts,
- setTargetedLabels,
- setTargetedTeams,
- setTargetsTotalCount,
- };
-
- const step3Props = {
- queryId,
- selectedTargets,
- storedQuery,
- setSelectedTargets,
- goToQueryEditor,
- targetsTotalCount,
- };
-
- switch (step) {
- case QUERIES_PAGE_STEPS[2]:
- return ;
- case QUERIES_PAGE_STEPS[3]:
- return ;
- default:
- return ;
- }
+ // Function instead of constant eliminates race condition
+ const backToQueriesPath = () => {
+ return queryId ? PATHS.QUERY(queryId) : PATHS.MANAGE_QUERIES;
};
- const isFirstStep = step === QUERIES_PAGE_STEPS[1];
const showSidebar =
- isFirstStep &&
isSidebarOpen &&
(isGlobalAdmin ||
isGlobalMaintainer ||
@@ -275,7 +282,31 @@ const QueryPage = ({
return (
<>
- {renderScreen()}
+
{showSidebar && (
@@ -290,4 +321,4 @@ const QueryPage = ({
);
};
-export default QueryPage;
+export default EditQueryPage;
diff --git a/frontend/pages/queries/edit/EditQueryPage/_styles.scss b/frontend/pages/queries/edit/EditQueryPage/_styles.scss
new file mode 100644
index 0000000000..f4ffcbbfd2
--- /dev/null
+++ b/frontend/pages/queries/edit/EditQueryPage/_styles.scss
@@ -0,0 +1,54 @@
+.edit-query-page {
+ .body-wrap {
+ min-width: 0;
+ }
+
+ &__warning {
+ padding: $pad-medium;
+ font-size: $x-small;
+ color: $core-fleet-black;
+ background-color: #fff0b9;
+ border: 1px solid #f2c94c;
+ border-radius: $border-radius;
+ margin: 0;
+ margin-top: $pad-large;
+
+ p {
+ margin: 0;
+ line-height: 20px;
+ }
+ }
+
+ .ace_content {
+ min-height: 500px !important;
+ }
+
+ &__count-spinner {
+ margin-right: $pad-small;
+ }
+ &__page-loading {
+ .loading-spinner {
+ margin: $pad-large 0 0;
+ }
+ }
+ &__page-error {
+ h4 {
+ margin: 0;
+ margin-top: 28px;
+ margin-left: -7px;
+ font-size: $small;
+
+ img {
+ transform: scale(0.5);
+ vertical-align: middle;
+ position: relative;
+ top: -2px;
+ }
+ }
+ p {
+ margin: 0;
+ margin-top: $pad-medium;
+ font-size: $x-small;
+ }
+ }
+}
diff --git a/frontend/pages/queries/edit/EditQueryPage/index.ts b/frontend/pages/queries/edit/EditQueryPage/index.ts
new file mode 100644
index 0000000000..29c0d100ac
--- /dev/null
+++ b/frontend/pages/queries/edit/EditQueryPage/index.ts
@@ -0,0 +1 @@
+export { default } from "./EditQueryPage";
diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tests.tsx b/frontend/pages/queries/edit/components/QueryForm/QueryForm.tests.tsx
similarity index 98%
rename from frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tests.tsx
rename to frontend/pages/queries/edit/components/QueryForm/QueryForm.tests.tsx
index 87bc29c8fd..bb5286895f 100644
--- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tests.tsx
+++ b/frontend/pages/queries/edit/components/QueryForm/QueryForm.tests.tsx
@@ -68,7 +68,6 @@ describe("QueryForm - component", () => {
isQueryUpdating={false}
saveQuery={jest.fn()}
onOsqueryTableSelect={jest.fn()}
- goToSelectTargets={jest.fn()}
onUpdate={jest.fn()}
onOpenSchemaSidebar={jest.fn()}
renderLiveQueryWarning={jest.fn()}
diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/edit/components/QueryForm/QueryForm.tsx
similarity index 95%
rename from frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx
rename to frontend/pages/queries/edit/components/QueryForm/QueryForm.tsx
index 452e06d4ee..a704ee2ef8 100644
--- a/frontend/pages/queries/QueryPage/components/QueryForm/QueryForm.tsx
+++ b/frontend/pages/queries/edit/components/QueryForm/QueryForm.tsx
@@ -15,7 +15,11 @@ import PATHS from "router/paths";
import { AppContext } from "context/app";
import { QueryContext } from "context/query";
import { NotificationContext } from "context/notification";
-import { addGravatarUrlToResource, secondsToDhms } from "utilities/helpers";
+import {
+ addGravatarUrlToResource,
+ secondsToDhms,
+ TAGGED_TEMPLATES,
+} from "utilities/helpers";
import {
FREQUENCY_DROPDOWN_OPTIONS,
SCHEDULE_PLATFORM_DROPDOWN_OPTIONS,
@@ -48,6 +52,7 @@ import Spinner from "components/Spinner";
import Icon from "components/Icon/Icon";
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
import SaveQueryModal from "../SaveQueryModal";
+import SaveChangesModal from "../SaveChangesModal";
const baseClass = "query-form";
@@ -63,11 +68,11 @@ interface IQueryFormProps {
isQueryUpdating: boolean;
saveQuery: (formData: ICreateQueryRequestBody) => void;
onOsqueryTableSelect: (tableName: string) => void;
- goToSelectTargets: () => void;
onUpdate: (formData: ICreateQueryRequestBody) => void;
onOpenSchemaSidebar: () => void;
renderLiveQueryWarning: () => JSX.Element | null;
backendValidators: { [key: string]: string };
+ hostId?: number;
}
const validateQuerySQL = (query: string) => {
@@ -110,11 +115,11 @@ const QueryForm = ({
isQueryUpdating,
saveQuery,
onOsqueryTableSelect,
- goToSelectTargets,
onUpdate,
onOpenSchemaSidebar,
renderLiveQueryWarning,
backendValidators,
+ hostId,
}: IQueryFormProps): JSX.Element => {
// Note: The QueryContext values should always be used for any mutable query data such as query name
// The storedQuery prop should only be used to access immutable metadata such as author id
@@ -153,6 +158,7 @@ const QueryForm = ({
const savedQueryMode = !!queryIdForEdit;
const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined
const [showSaveQueryModal, setShowSaveQueryModal] = useState(false);
+ const [showSaveChangesModal, setShowSaveChangesModal] = useState(false); // #7766 implementation
const [showQueryEditor, setShowQueryEditor] = useState(
isObserverPlus || isAnyTeamObserverPlus || false
);
@@ -205,6 +211,11 @@ const QueryForm = ({
setShowSaveQueryModal(!showSaveQueryModal);
};
+ // #7766 implementation
+ const toggleSaveChangesModal = () => {
+ setShowSaveChangesModal(!showSaveChangesModal);
+ };
+
const onLoad = (editor: IAceEditor) => {
editor.setOptions({
enableLinking: true,
@@ -400,6 +411,12 @@ const QueryForm = ({
logging: lastEditedQueryLoggingType,
});
}
+
+ // #7766 implementation
+ // savedQueryMode
+ // ? setShowSaveChangesModal(true)
+ // : setShowSaveQueryModal(true);
+ // TODO: onUpdate for saveChangesModal
}
};
@@ -575,7 +592,13 @@ const QueryForm = ({
@@ -749,7 +772,13 @@ const QueryForm = ({
@@ -765,6 +794,13 @@ const QueryForm = ({
isLoading={isQuerySaving}
/>
)}
+ {showSaveChangesModal && (
+
+ )}
>
);
};
diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss b/frontend/pages/queries/edit/components/QueryForm/_styles.scss
similarity index 98%
rename from frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss
rename to frontend/pages/queries/edit/components/QueryForm/_styles.scss
index 5a9ab0d49f..bc63146bac 100644
--- a/frontend/pages/queries/QueryPage/components/QueryForm/_styles.scss
+++ b/frontend/pages/queries/edit/components/QueryForm/_styles.scss
@@ -3,11 +3,6 @@
position: relative;
font-size: $x-small;
- .query-page__warning {
- margin: 0;
- margin-top: $pad-large;
- }
-
.form-field--input {
margin: 0;
}
diff --git a/frontend/pages/queries/QueryPage/components/QueryForm/index.ts b/frontend/pages/queries/edit/components/QueryForm/index.ts
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/QueryForm/index.ts
rename to frontend/pages/queries/edit/components/QueryForm/index.ts
diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/QueryResults.tsx b/frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/QueryResults/QueryResults.tsx
rename to frontend/pages/queries/edit/components/QueryResults/QueryResults.tsx
diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx b/frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/QueryResults/QueryResultsTableConfig.tsx
rename to frontend/pages/queries/edit/components/QueryResults/QueryResultsTableConfig.tsx
diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/_styles.scss b/frontend/pages/queries/edit/components/QueryResults/_styles.scss
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/QueryResults/_styles.scss
rename to frontend/pages/queries/edit/components/QueryResults/_styles.scss
diff --git a/frontend/pages/queries/QueryPage/components/QueryResults/index.ts b/frontend/pages/queries/edit/components/QueryResults/index.ts
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/QueryResults/index.ts
rename to frontend/pages/queries/edit/components/QueryResults/index.ts
diff --git a/frontend/pages/queries/edit/components/SaveChangesModal/SaveChangesModal.tsx b/frontend/pages/queries/edit/components/SaveChangesModal/SaveChangesModal.tsx
new file mode 100644
index 0000000000..362af3918d
--- /dev/null
+++ b/frontend/pages/queries/edit/components/SaveChangesModal/SaveChangesModal.tsx
@@ -0,0 +1,53 @@
+import React from "react";
+
+import Button from "components/buttons/Button";
+import Modal from "components/Modal";
+import { ICreateQueryRequestBody } from "interfaces/schedulable_query";
+
+const baseClass = "save-changes-modal";
+
+export interface ISaveChangesModalProps {
+ isUpdating: boolean;
+ onSaveChanges: (formData: ICreateQueryRequestBody) => void;
+ toggleSaveChangesModal: () => void;
+ sqlUpdated?: boolean;
+}
+
+const SaveChangesModal = ({
+ isUpdating,
+ onSaveChanges,
+ toggleSaveChangesModal,
+ sqlUpdated = false,
+}: ISaveChangesModalProps): JSX.Element => {
+ const warningText = () => {
+ if (sqlUpdated) {
+ return "Changing this query's SQL will delete its previous results, since the existing report does not reflect the updated query.";
+ }
+ return "The changes you are making to this query will delete its previous results.";
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default SaveChangesModal;
diff --git a/frontend/pages/queries/edit/components/SaveChangesModal/index.ts b/frontend/pages/queries/edit/components/SaveChangesModal/index.ts
new file mode 100644
index 0000000000..15fa4a05cf
--- /dev/null
+++ b/frontend/pages/queries/edit/components/SaveChangesModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./SaveChangesModal";
diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx b/frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tsx
similarity index 51%
rename from frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx
rename to frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tsx
index f74a45f66c..a8cd8dc099 100644
--- a/frontend/pages/queries/QueryPage/components/SaveQueryModal/SaveQueryModal.tsx
+++ b/frontend/pages/queries/edit/components/SaveQueryModal/SaveQueryModal.tsx
@@ -141,116 +141,114 @@ const SaveQueryModal = ({
return (
- <>
-
);
};
diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/_styles.scss b/frontend/pages/queries/edit/components/SaveQueryModal/_styles.scss
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/SaveQueryModal/_styles.scss
rename to frontend/pages/queries/edit/components/SaveQueryModal/_styles.scss
diff --git a/frontend/pages/queries/QueryPage/components/SaveQueryModal/index.ts b/frontend/pages/queries/edit/components/SaveQueryModal/index.ts
similarity index 100%
rename from frontend/pages/queries/QueryPage/components/SaveQueryModal/index.ts
rename to frontend/pages/queries/edit/components/SaveQueryModal/index.ts
diff --git a/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx b/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx
new file mode 100644
index 0000000000..c49d4c333b
--- /dev/null
+++ b/frontend/pages/queries/live/LiveQueryPage/LiveQueryPage.tsx
@@ -0,0 +1,213 @@
+import React, { useState, useEffect, useContext, useCallback } from "react";
+import { useQuery } from "react-query";
+import { useErrorHandler } from "react-error-boundary";
+import { InjectedRouter, Params } from "react-router/lib/Router";
+import PATHS from "router/paths";
+
+import { AppContext } from "context/app";
+import { QueryContext } from "context/query";
+import { LIVE_QUERY_STEPS, DEFAULT_QUERY } from "utilities/constants";
+import queryAPI from "services/entities/queries";
+import hostAPI from "services/entities/hosts";
+import statusAPI from "services/entities/status";
+import { IHost, IHostResponse } from "interfaces/host";
+import { ILabel } from "interfaces/label";
+import { ITeam } from "interfaces/team";
+import {
+ IGetQueryResponse,
+ ISchedulableQuery,
+} from "interfaces/schedulable_query";
+
+import MainContent from "components/MainContent";
+import SelectTargets from "components/LiveQuery/SelectTargets";
+
+import RunQuery from "pages/queries/live/screens/RunQuery";
+import useTeamIdParam from "hooks/useTeamIdParam";
+
+interface IRunQueryPageProps {
+ router: InjectedRouter;
+ params: Params;
+ location: {
+ pathname: string;
+ query: { host_ids: string; team_id?: string };
+ search: string;
+ };
+}
+
+const baseClass = "run-query-page";
+
+const RunQueryPage = ({
+ router,
+ params: { id: paramsQueryId },
+ location,
+}: IRunQueryPageProps): JSX.Element => {
+ const queryId = paramsQueryId ? parseInt(paramsQueryId, 10) : null;
+ const {
+ currentTeamName: teamNameForQuery,
+ teamIdForApi: apiTeamIdForQuery,
+ } = useTeamIdParam({
+ location,
+ router,
+ includeAllTeams: true,
+ includeNoTeam: false,
+ });
+
+ const handlePageError = useErrorHandler();
+ const {
+ isGlobalAdmin,
+ isGlobalMaintainer,
+ isAnyTeamMaintainerOrTeamAdmin,
+ isObserverPlus,
+ isAnyTeamObserverPlus,
+ } = useContext(AppContext);
+ const {
+ selectedQueryTargets,
+ setSelectedQueryTargets,
+ setLastEditedQueryId,
+ setLastEditedQueryName,
+ setLastEditedQueryDescription,
+ setLastEditedQueryBody,
+ setLastEditedQueryObserverCanRun,
+ setLastEditedQueryFrequency,
+ setLastEditedQueryLoggingType,
+ setLastEditedQueryMinOsqueryVersion,
+ setLastEditedQueryPlatforms,
+ } = useContext(QueryContext);
+
+ const [queryParamHostsAdded, setQueryParamHostsAdded] = useState(false);
+ const [step, setStep] = useState(LIVE_QUERY_STEPS[1]);
+ const [targetedHosts, setTargetedHosts] = useState([]);
+ const [targetedLabels, setTargetedLabels] = useState([]);
+ const [targetedTeams, setTargetedTeams] = useState([]);
+ const [targetsTotalCount, setTargetsTotalCount] = useState(0);
+ const [isLiveQueryRunnable, setIsLiveQueryRunnable] = useState(true);
+
+ const TAGGED_TEMPLATES = {
+ queryByHostRoute: (hostId: number | undefined | null) => {
+ return `${hostId ? `?host_ids=${hostId}` : ""}`;
+ },
+ };
+
+ // disabled on page load so we can control the number of renders
+ // else it will re-populate the context on occasion
+ const { data: storedQuery } = useQuery<
+ IGetQueryResponse,
+ Error,
+ ISchedulableQuery
+ >(["query", queryId], () => queryAPI.load(queryId as number), {
+ enabled: !!queryId,
+ refetchOnWindowFocus: false,
+ select: (data) => data.query,
+ onSuccess: (returnedQuery) => {
+ setLastEditedQueryId(returnedQuery.id);
+ setLastEditedQueryName(returnedQuery.name);
+ setLastEditedQueryDescription(returnedQuery.description);
+ setLastEditedQueryBody(returnedQuery.query);
+ setLastEditedQueryObserverCanRun(returnedQuery.observer_can_run);
+ setLastEditedQueryFrequency(returnedQuery.interval);
+ setLastEditedQueryPlatforms(returnedQuery.platform);
+ setLastEditedQueryLoggingType(returnedQuery.logging);
+ setLastEditedQueryMinOsqueryVersion(returnedQuery.min_osquery_version);
+ },
+ onError: (error) => handlePageError(error),
+ });
+
+ useQuery(
+ "hostFromURL",
+ () =>
+ hostAPI.loadHostDetails(parseInt(location.query.host_ids as string, 10)),
+ {
+ enabled: !!location.query.host_ids && !queryParamHostsAdded,
+ select: (data: IHostResponse) => data.host,
+ onSuccess: (host) => {
+ setTargetedHosts((prevHosts) =>
+ prevHosts.filter((h) => h.id !== host.id).concat(host)
+ );
+ const targets = selectedQueryTargets;
+ host.target_type = "hosts";
+ targets.push(host);
+ setSelectedQueryTargets([...targets]);
+ if (!queryParamHostsAdded) {
+ setQueryParamHostsAdded(true);
+ }
+ router.replace(location.pathname);
+ },
+ }
+ );
+
+ const detectIsFleetQueryRunnable = () => {
+ statusAPI.live_query().catch(() => {
+ setIsLiveQueryRunnable(false);
+ });
+ };
+
+ useEffect(() => {
+ detectIsFleetQueryRunnable();
+ if (!queryId) {
+ setLastEditedQueryId(DEFAULT_QUERY.id);
+ setLastEditedQueryName(DEFAULT_QUERY.name);
+ setLastEditedQueryDescription(DEFAULT_QUERY.description);
+ setLastEditedQueryBody(DEFAULT_QUERY.query);
+ setLastEditedQueryObserverCanRun(DEFAULT_QUERY.observer_can_run);
+ setLastEditedQueryFrequency(DEFAULT_QUERY.interval);
+ setLastEditedQueryLoggingType(DEFAULT_QUERY.logging);
+ setLastEditedQueryMinOsqueryVersion(DEFAULT_QUERY.min_osquery_version);
+ setLastEditedQueryPlatforms(DEFAULT_QUERY.platform);
+ }
+ }, [queryId]);
+
+ // Updates title that shows up on browser tabs
+ useEffect(() => {
+ // e.g., Run live query | Discover TLS certificates | Fleet for osquery
+ document.title = `Run live query | ${storedQuery?.name} | Fleet for osquery`;
+ }, [location.pathname, storedQuery?.name]);
+
+ const goToQueryEditor = useCallback(
+ () => queryId && router.push(PATHS.EDIT_QUERY(queryId)),
+ []
+ );
+ // const params = { id: paramsQueryId };
+
+ const renderScreen = () => {
+ const step1Props = {
+ baseClass,
+ queryId,
+ selectedTargets: selectedQueryTargets,
+ targetedHosts,
+ targetedLabels,
+ targetedTeams,
+ targetsTotalCount,
+ goToQueryEditor,
+ goToRunQuery: () => setStep(LIVE_QUERY_STEPS[2]),
+ setSelectedTargets: setSelectedQueryTargets,
+ setTargetedHosts,
+ setTargetedLabels,
+ setTargetedTeams,
+ setTargetsTotalCount,
+ };
+
+ const step2Props = {
+ queryId,
+ selectedTargets: selectedQueryTargets,
+ storedQuery,
+ setSelectedTargets: setSelectedQueryTargets,
+ goToQueryEditor,
+ targetsTotalCount,
+ };
+
+ switch (step) {
+ case LIVE_QUERY_STEPS[2]:
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+ {renderScreen()}
+
+ );
+};
+
+export default RunQueryPage;
diff --git a/frontend/pages/queries/QueryPage/_styles.scss b/frontend/pages/queries/live/LiveQueryPage/_styles.scss
similarity index 69%
rename from frontend/pages/queries/QueryPage/_styles.scss
rename to frontend/pages/queries/live/LiveQueryPage/_styles.scss
index 8e1198b7f8..1ec2385d09 100644
--- a/frontend/pages/queries/QueryPage/_styles.scss
+++ b/frontend/pages/queries/live/LiveQueryPage/_styles.scss
@@ -1,4 +1,4 @@
-.query-page {
+.run-query-page {
.body-wrap {
min-width: 0;
}
@@ -9,61 +9,6 @@
min-height: 400px;
}
- &__warning {
- padding: $pad-medium;
- font-size: $x-small;
- color: $core-fleet-black;
- background-color: #fff0b9;
- border: 1px solid #f2c94c;
- border-radius: $border-radius;
-
- p {
- margin: 0;
- line-height: 20px;
- }
- }
-
- &__observer-query-view {
- width: 90%;
- max-width: 1060px;
- margin: 0 auto;
- color: $core-fleet-black;
-
- h1 {
- font-size: $medium;
- }
- p {
- font-size: $x-small;
- }
- }
-
- &__observer-query-details {
- padding: 0 2rem;
-
- h1 {
- margin: $pad-large 0;
- font-size: $large;
- }
-
- p {
- margin-bottom: $pad-small;
- }
-
- .sql-button {
- color: $core-vibrant-blue;
- font-weight: $bold;
- font-size: $x-small;
- }
- }
-
- &__query-preview {
- margin-top: 15px;
-
- .fleet-ace__label {
- display: none;
- }
- }
-
.ace_content {
min-height: 500px !important;
}
@@ -172,10 +117,4 @@
font-size: $x-small;
}
}
-
- .targets-input {
- .input-icon-field__icon {
- top: 34px; // Override styling to include label header
- }
- }
}
diff --git a/frontend/pages/queries/live/LiveQueryPage/index.ts b/frontend/pages/queries/live/LiveQueryPage/index.ts
new file mode 100644
index 0000000000..354c0445d1
--- /dev/null
+++ b/frontend/pages/queries/live/LiveQueryPage/index.ts
@@ -0,0 +1 @@
+export { default } from "./LiveQueryPage";
diff --git a/frontend/pages/queries/QueryPage/screens/RunQuery.tsx b/frontend/pages/queries/live/screens/RunQuery.tsx
similarity index 98%
rename from frontend/pages/queries/QueryPage/screens/RunQuery.tsx
rename to frontend/pages/queries/live/screens/RunQuery.tsx
index dbceec8069..7715899b34 100644
--- a/frontend/pages/queries/QueryPage/screens/RunQuery.tsx
+++ b/frontend/pages/queries/live/screens/RunQuery.tsx
@@ -15,7 +15,7 @@ import { ICampaign, ICampaignState } from "interfaces/campaign";
import { IQuery } from "interfaces/query";
import { ITarget } from "interfaces/target";
-import QueryResults from "../components/QueryResults";
+import QueryResults from "../../edit/components/QueryResults";
interface IRunQueryProps {
storedQuery: IQuery | undefined;
diff --git a/frontend/router/index.tsx b/frontend/router/index.tsx
index f314b5ecc8..07f1256436 100644
--- a/frontend/router/index.tsx
+++ b/frontend/router/index.tsx
@@ -36,7 +36,9 @@ import ManagePoliciesPage from "pages/policies/ManagePoliciesPage";
import NoAccessPage from "pages/NoAccessPage";
import PackComposerPage from "pages/packs/PackComposerPage";
import PolicyPage from "pages/policies/PolicyPage";
-import QueryPage from "pages/queries/QueryPage";
+import QueryDetailsPage from "pages/queries/details/QueryDetailsPage";
+import LiveQueryPage from "pages/queries/live/LiveQueryPage";
+import EditQueryPage from "pages/queries/edit/EditQueryPage";
import RegistrationPage from "pages/RegistrationPage";
import ResetPasswordPage from "pages/ResetPasswordPage";
import MDMAppleSSOPage from "pages/MDMAppleSSOPage";
@@ -220,9 +222,13 @@ const routes = (
-
+
+
+
+
+
+
-
diff --git a/frontend/router/paths.ts b/frontend/router/paths.ts
index 834403705e..3190f09c45 100644
--- a/frontend/router/paths.ts
+++ b/frontend/router/paths.ts
@@ -53,6 +53,16 @@ export default {
return `${URL_PREFIX}/labels/${labelId}`;
},
EDIT_QUERY: (queryId: number, teamId?: number): string => {
+ return `${URL_PREFIX}/queries/${queryId}/edit${
+ teamId ? `?team_id=${teamId}` : ""
+ }`;
+ },
+ LIVE_QUERY: (queryId: number, teamId?: number): string => {
+ return `${URL_PREFIX}/queries/${queryId}/live${
+ teamId ? `?team_id=${teamId}` : ""
+ }`;
+ },
+ QUERY: (queryId: number, teamId?: number): string => {
return `${URL_PREFIX}/queries/${queryId}${
teamId ? `?team_id=${teamId}` : ""
}`;
diff --git a/frontend/services/entities/queries.ts b/frontend/services/entities/queries.ts
index 89765f6481..bfad2ff9ed 100644
--- a/frontend/services/entities/queries.ts
+++ b/frontend/services/entities/queries.ts
@@ -54,10 +54,10 @@ export default {
queryId: number | null;
selected: ISelectedTargets;
}) => {
- const { RUN_QUERY } = endpoints;
+ const { LIVE_QUERY } = endpoints;
try {
- const { campaign } = await sendRequest("POST", RUN_QUERY, {
+ const { campaign } = await sendRequest("POST", LIVE_QUERY, {
query,
query_id: queryId,
selected,
diff --git a/frontend/utilities/constants.ts b/frontend/utilities/constants.ts
index 14530e240b..079e929753 100644
--- a/frontend/utilities/constants.ts
+++ b/frontend/utilities/constants.ts
@@ -96,12 +96,17 @@ export const MIN_OSQUERY_VERSION_OPTIONS = [
{ label: "1.8.1 +", value: "1.8.1" },
];
-export const QUERIES_PAGE_STEPS = {
+export const LIVE_POLICY_STEPS = {
1: "EDITOR",
2: "TARGETS",
3: "RUN",
};
+export const LIVE_QUERY_STEPS = {
+ 1: "TARGETS",
+ 2: "RUN",
+};
+
export const DEFAULT_QUERY: ISchedulableQuery = {
description: "",
name: "",
diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts
index ce6d89b952..e1a3880b99 100644
--- a/frontend/utilities/endpoints.ts
+++ b/frontend/utilities/endpoints.ts
@@ -82,7 +82,7 @@ export default {
PERFORM_REQUIRED_PASSWORD_RESET: `/${API_VERSION}/fleet/perform_required_password_reset`,
QUERIES: `/${API_VERSION}/fleet/queries`,
RESET_PASSWORD: `/${API_VERSION}/fleet/reset_password`,
- RUN_QUERY: `/${API_VERSION}/fleet/queries/run`,
+ LIVE_QUERY: `/${API_VERSION}/fleet/queries/run`,
SCHEDULE_QUERY: `/${API_VERSION}/fleet/packs/schedule`,
SCHEDULED_QUERIES: (packId: number): string => {
return `/${API_VERSION}/fleet/packs/${packId}/scheduled`;
diff --git a/frontend/utilities/helpers.ts b/frontend/utilities/helpers.ts
index 641b5cf734..c519b9a263 100644
--- a/frontend/utilities/helpers.ts
+++ b/frontend/utilities/helpers.ts
@@ -910,6 +910,12 @@ export const getSoftwareBundleTooltipMarkup = (bundle: string) => {
`;
};
+export const TAGGED_TEMPLATES = {
+ queryByHostRoute: (hostId: number | undefined | null) => {
+ return `${hostId ? `?host_ids=${hostId}` : ""}`;
+ },
+};
+
export default {
addGravatarUrlToResource,
formatConfigDataForServer,
@@ -945,4 +951,5 @@ export default {
syntaxHighlight,
normalizeEmptyValues,
wrapFleetHelper,
+ TAGGED_TEMPLATES,
};
From be0ce917c113976f85e9ebae161b165cfb51cc73 Mon Sep 17 00:00:00 2001
From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com>
Date: Wed, 4 Oct 2023 11:31:26 -0700
Subject: [PATCH 02/18] UI - Add global "Disable query reports" setting to
advanced org settings (#14268)
## Addresses #13474
- small alignment issues with tooltip-wrapped text should be fixed by
upcoming TooltipWrapper refactor PR
## Checklist for submitter
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling
---
frontend/__mocks__/configMock.ts | 1 +
frontend/interfaces/config.ts | 1 +
.../cards/Advanced/Advanced.tsx | 24 ++++++++++++++++++-
3 files changed, 25 insertions(+), 1 deletion(-)
diff --git a/frontend/__mocks__/configMock.ts b/frontend/__mocks__/configMock.ts
index c51d2895d5..9f9e8f638c 100644
--- a/frontend/__mocks__/configMock.ts
+++ b/frontend/__mocks__/configMock.ts
@@ -12,6 +12,7 @@ const DEFAULT_CONFIG_MOCK: IConfig = {
live_query_disabled: false,
enable_analytics: true,
deferred_save_host: false,
+ query_reports_disabled: false,
},
smtp_settings: {
enable_smtp: false,
diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts
index c604629c0c..1d42508ee4 100644
--- a/frontend/interfaces/config.ts
+++ b/frontend/interfaces/config.ts
@@ -196,6 +196,7 @@ export interface IConfig {
live_query_disabled: boolean;
enable_analytics: boolean;
deferred_save_host: boolean;
+ query_reports_disabled: boolean;
};
smtp_settings: {
enable_smtp: boolean;
diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx
index 61da4d5b8d..62f6d4d4b5 100644
--- a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx
+++ b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx
@@ -18,7 +18,7 @@ const Advanced = ({
handleSubmit,
isUpdatingSettings,
}: IAppConfigFormProps): JSX.Element => {
- const [formData, setFormData] = useState({
+ const [formData, setFormData] = useState({
domain: appConfig.smtp_settings.domain || "",
verifySSLCerts: appConfig.smtp_settings.verify_ssl_certs || false,
enableStartTLS: appConfig.smtp_settings.enable_start_tls,
@@ -26,6 +26,8 @@ const Advanced = ({
appConfig.host_expiry_settings.host_expiry_enabled || false,
hostExpiryWindow: appConfig.host_expiry_settings.host_expiry_window || 0,
disableLiveQuery: appConfig.server_settings.live_query_disabled || false,
+ disableQueryReports:
+ appConfig.server_settings.query_reports_disabled || false,
});
const {
@@ -35,6 +37,7 @@ const Advanced = ({
enableHostExpiry,
hostExpiryWindow,
disableLiveQuery,
+ disableQueryReports,
} = formData;
const [formErrors, setFormErrors] = useState({});
@@ -69,6 +72,7 @@ const Advanced = ({
server_url: appConfig.server_settings.server_url || "",
live_query_disabled: disableLiveQuery,
enable_analytics: appConfig.server_settings.enable_analytics,
+ query_reports_disabled: disableQueryReports,
},
smtp_settings: {
enable_smtp: appConfig.smtp_settings.enable_smtp || false,
@@ -172,6 +176,24 @@ const Advanced = ({
>
Disable live queries
+ Disabling query reports will decrease database usage,
\
+ but will prevent you from accessing query results in
\
+ Fleet and will delete existing reports. This can also be
\
+ disabled on a per-query basis by enabling "Discard
\
+ data". (Default: Off)
'
+ }
+ >
+ Disable query reports
+
From 5ed443590e2bb935e3ee450df034740eff067c92 Mon Sep 17 00:00:00 2001
From: RachelElysia <71795832+RachelElysia@users.noreply.github.com>
Date: Wed, 4 Oct 2023 14:35:18 -0700
Subject: [PATCH 03/18] Fleet UI: Surface delete previous results modals
(#14257)
---
.../QueryDetailsPage/QueryDetailsPage.tsx | 4 +-
.../edit/EditQueryPage/EditQueryPage.tsx | 7 ++-
.../components/QueryForm/QueryForm.tests.tsx | 2 +
.../edit/components/QueryForm/QueryForm.tsx | 47 ++++++++++++++-----
.../SaveChangesModal/SaveChangesModal.tsx | 2 +-
5 files changed, 46 insertions(+), 16 deletions(-)
diff --git a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
index be52bd6ad3..17c46c3ade 100644
--- a/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
+++ b/frontend/pages/queries/details/QueryDetailsPage/QueryDetailsPage.tsx
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect } from "react";
+import React, { useContext } from "react";
import { useQuery } from "react-query";
import { InjectedRouter, Params } from "react-router/lib/Router";
import { useErrorHandler } from "react-error-boundary";
@@ -112,7 +112,7 @@ const QueryDetailsPage = ({
);
const isLoading = isStoredQueryLoading; // TODO: Add || isCachedResultsLoading for new API response
- const isApiError = storedQueryError || true; // TODO: Add || isCachedResultsError for new API response
+ const isApiError = storedQueryError || false; // TODO: Add || isCachedResultsError for new API response
const renderHeader = () => {
const canEditQuery =
diff --git a/frontend/pages/queries/edit/EditQueryPage/EditQueryPage.tsx b/frontend/pages/queries/edit/EditQueryPage/EditQueryPage.tsx
index e9aa57dce9..59b14feb72 100644
--- a/frontend/pages/queries/edit/EditQueryPage/EditQueryPage.tsx
+++ b/frontend/pages/queries/edit/EditQueryPage/EditQueryPage.tsx
@@ -100,13 +100,14 @@ const EditQueryPage = ({
const [showOpenSchemaActionText, setShowOpenSchemaActionText] = useState(
false
);
+ const [showSaveChangesModal, setShowSaveChangesModal] = useState(false);
// disabled on page load so we can control the number of renders
// else it will re-populate the context on occasion
const {
isLoading: isStoredQueryLoading,
data: storedQuery,
- error: storedQueryError,
+ refetch: refetchStoredQuery,
} = useQuery(
["query", queryId],
() => queryAPI.load(queryId as number),
@@ -215,6 +216,7 @@ const EditQueryPage = ({
try {
await queryAPI.update(queryId, updatedQuery);
renderFlash("success", "Query updated!");
+ refetchStoredQuery(); // Required to compare recently saved query to a subsequent save to the query
} catch (updateError: any) {
console.error(updateError);
if (updateError.data.errors[0].reason.includes("Duplicate")) {
@@ -228,6 +230,7 @@ const EditQueryPage = ({
}
setIsQueryUpdating(false);
+ setShowSaveChangesModal(false); // Closes conditionally opened modal when discarding previous results
return false;
};
@@ -304,6 +307,8 @@ const EditQueryPage = ({
isQuerySaving={isQuerySaving}
isQueryUpdating={isQueryUpdating}
hostId={parseInt(location.query.host_ids as string, 10)}
+ showSaveChangesModal={showSaveChangesModal}
+ setShowSaveChangesModal={setShowSaveChangesModal}
/>
diff --git a/frontend/pages/queries/edit/components/QueryForm/QueryForm.tests.tsx b/frontend/pages/queries/edit/components/QueryForm/QueryForm.tests.tsx
index bb5286895f..f3655dd3f9 100644
--- a/frontend/pages/queries/edit/components/QueryForm/QueryForm.tests.tsx
+++ b/frontend/pages/queries/edit/components/QueryForm/QueryForm.tests.tsx
@@ -72,6 +72,8 @@ describe("QueryForm - component", () => {
onOpenSchemaSidebar={jest.fn()}
renderLiveQueryWarning={jest.fn()}
backendValidators={{}}
+ showSaveChangesModal={false}
+ setShowSaveChangesModal={jest.fn()}
/>
);
diff --git a/frontend/pages/queries/edit/components/QueryForm/QueryForm.tsx b/frontend/pages/queries/edit/components/QueryForm/QueryForm.tsx
index a704ee2ef8..f5d269b1d2 100644
--- a/frontend/pages/queries/edit/components/QueryForm/QueryForm.tsx
+++ b/frontend/pages/queries/edit/components/QueryForm/QueryForm.tsx
@@ -73,6 +73,8 @@ interface IQueryFormProps {
renderLiveQueryWarning: () => JSX.Element | null;
backendValidators: { [key: string]: string };
hostId?: number;
+ showSaveChangesModal: boolean;
+ setShowSaveChangesModal: (bool: boolean) => void;
}
const validateQuerySQL = (query: string) => {
@@ -120,6 +122,8 @@ const QueryForm = ({
renderLiveQueryWarning,
backendValidators,
hostId,
+ showSaveChangesModal,
+ setShowSaveChangesModal,
}: IQueryFormProps): JSX.Element => {
// Note: The QueryContext values should always be used for any mutable query data such as query name
// The storedQuery prop should only be used to access immutable metadata such as author id
@@ -158,7 +162,6 @@ const QueryForm = ({
const savedQueryMode = !!queryIdForEdit;
const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined
const [showSaveQueryModal, setShowSaveQueryModal] = useState(false);
- const [showSaveChangesModal, setShowSaveChangesModal] = useState(false); // #7766 implementation
const [showQueryEditor, setShowQueryEditor] = useState(
isObserverPlus || isAnyTeamObserverPlus || false
);
@@ -211,7 +214,6 @@ const QueryForm = ({
setShowSaveQueryModal(!showSaveQueryModal);
};
- // #7766 implementation
const toggleSaveChangesModal = () => {
setShowSaveChangesModal(!showSaveChangesModal);
};
@@ -411,12 +413,6 @@ const QueryForm = ({
logging: lastEditedQueryLoggingType,
});
}
-
- // #7766 implementation
- // savedQueryMode
- // ? setShowSaveChangesModal(true)
- // : setShowSaveQueryModal(true);
- // TODO: onUpdate for saveChangesModal
}
};
@@ -609,6 +605,26 @@ const QueryForm = ({
const hasSavePermissions = isGlobalAdmin || isGlobalMaintainer;
+ const hasSqlChange = storedQuery && lastEditedQueryBody !== storedQuery.query;
+ const hasSnapshotChange =
+ storedQuery &&
+ lastEditedQueryLoggingType !== "snapshot" &&
+ storedQuery.logging === "snapshot";
+ // Use commented out logic when discard data checkbox is implemented #13470
+ const hasEnabledDiscardData = false;
+ // const hasEnabledDiscardData =
+ // storedQuery && lastEditedDiscardData && !storedQuery.discardData;
+
+ const confirmChanges = (): boolean => {
+ // Confirm changes if the query has been edited, removed snapshot logging, or enabled discard data
+ return hasSqlChange || hasSnapshotChange || hasEnabledDiscardData;
+ };
+
+ const confirmSqlChange = (): boolean => {
+ // Confirm sql changes message if sql changed but snapshot and enabling discard data has not
+ return !!hasSqlChange && !hasSnapshotChange && !hasEnabledDiscardData;
+ };
+
// Global admin, any maintainer, any observer+ on new query
const renderEditableQueryForm = () => {
// Save disabled for team maintainer/admins viewing global queries
@@ -640,7 +656,9 @@ const QueryForm = ({
onLoad={onLoad}
wrapperClassName={`${baseClass}__text-editor-wrapper`}
onChange={onChangeQuery}
- handleSubmit={promptSaveQuery}
+ handleSubmit={
+ confirmChanges() ? toggleSaveChangesModal : promptSaveQuery
+ }
wrapEnabled
focus={!savedQueryMode}
/>
@@ -743,7 +761,11 @@ const QueryForm = ({
)}
>
diff --git a/frontend/pages/queries/edit/components/SaveChangesModal/SaveChangesModal.tsx b/frontend/pages/queries/edit/components/SaveChangesModal/SaveChangesModal.tsx
index 362af3918d..aa3601e903 100644
--- a/frontend/pages/queries/edit/components/SaveChangesModal/SaveChangesModal.tsx
+++ b/frontend/pages/queries/edit/components/SaveChangesModal/SaveChangesModal.tsx
@@ -8,7 +8,7 @@ const baseClass = "save-changes-modal";
export interface ISaveChangesModalProps {
isUpdating: boolean;
- onSaveChanges: (formData: ICreateQueryRequestBody) => void;
+ onSaveChanges: (evt: React.MouseEvent) => void;
toggleSaveChangesModal: () => void;
sqlUpdated?: boolean;
}
From df2ada180921bcfcbd189dc0f38511c7de6efb5d Mon Sep 17 00:00:00 2001
From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com>
Date: Wed, 4 Oct 2023 15:19:26 -0700
Subject: [PATCH 04/18] UI - Add 'Discard data' option to Save Query modal
(#14284)
## Addresses pt.1 of #13470
4 states + InfoBanner:
## Checklist for submitter
- [x] Manual QA for all new/changed functionality
---------
Co-authored-by: Jacob Shandling
---
frontend/__mocks__/queryMock.ts | 1 +
frontend/__mocks__/scheduleableQueryMock.ts | 1 +
frontend/components/InfoBanner/InfoBanner.tsx | 6 +-
frontend/components/InfoBanner/_styles.scss | 10 +-
.../components/buttons/Button/_styles.scss | 4 +-
frontend/components/icons/Chevron.tsx | 7 +-
frontend/interfaces/schedulable_query.ts | 4 +
.../edit/EditQueryPage/EditQueryPage.tsx | 17 ++-
.../edit/components/QueryForm/QueryForm.tsx | 7 ++
.../SaveQueryModal/SaveQueryModal.tsx | 105 ++++++++++++++++--
.../components/SaveQueryModal/_styles.scss | 19 ++++
frontend/styles/var/mixins.scss | 11 +-
frontend/utilities/constants.ts | 1 +
13 files changed, 170 insertions(+), 23 deletions(-)
diff --git a/frontend/__mocks__/queryMock.ts b/frontend/__mocks__/queryMock.ts
index df90eae93a..4d44c53347 100644
--- a/frontend/__mocks__/queryMock.ts
+++ b/frontend/__mocks__/queryMock.ts
@@ -12,6 +12,7 @@ const DEFAULT_QUERY_MOCK: ISchedulableQuery = {
author_name: "Test User",
author_email: "test@example.com",
observer_can_run: false,
+ discard_data: false,
interval: 300,
packs: [],
team_id: null,
diff --git a/frontend/__mocks__/scheduleableQueryMock.ts b/frontend/__mocks__/scheduleableQueryMock.ts
index edc82a61da..3ac57bb49d 100644
--- a/frontend/__mocks__/scheduleableQueryMock.ts
+++ b/frontend/__mocks__/scheduleableQueryMock.ts
@@ -20,6 +20,7 @@ const DEFAULT_SCHEDULABLE_QUERY_MOCK: ISchedulableQuery = {
author_name: "Test User",
author_email: "test@example.com",
observer_can_run: false,
+ discard_data: false,
packs: [],
stats: {
system_time_p50: 28.1053,
diff --git a/frontend/components/InfoBanner/InfoBanner.tsx b/frontend/components/InfoBanner/InfoBanner.tsx
index 1eb8d6d021..f1af1558b5 100644
--- a/frontend/components/InfoBanner/InfoBanner.tsx
+++ b/frontend/components/InfoBanner/InfoBanner.tsx
@@ -10,7 +10,7 @@ export interface IInfoBannerProps {
children?: React.ReactNode;
className?: string;
/** default light purple */
- color?: "yellow" | "grey";
+ color?: "purple" | "purple-bold-border" | "yellow" | "grey";
pageLevel?: boolean;
/** cta and link are mutually exclusive */
cta?: JSX.Element;
@@ -22,7 +22,7 @@ export interface IInfoBannerProps {
const InfoBanner = ({
children,
className,
- color,
+ color = "purple",
pageLevel,
cta,
closable,
@@ -30,8 +30,8 @@ const InfoBanner = ({
}: IInfoBannerProps): JSX.Element => {
const wrapperClasses = classNames(
baseClass,
+ `${baseClass}__${color}`,
{
- [`${baseClass}__${color}`]: !!color,
[`${baseClass}__page-banner`]: !!pageLevel,
},
className
diff --git a/frontend/components/InfoBanner/_styles.scss b/frontend/components/InfoBanner/_styles.scss
index 5902cba3db..f5977b57f5 100644
--- a/frontend/components/InfoBanner/_styles.scss
+++ b/frontend/components/InfoBanner/_styles.scss
@@ -5,11 +5,19 @@
padding: $pad-medium;
border-radius: $border-radius;
border: 1px solid $ui-vibrant-blue-50;
- background-color: $ui-vibrant-blue-10;
font-size: $x-small;
font-weight: $regular;
color: $core-fleet-black;
+ &__purple {
+ background-color: $ui-vibrant-blue-10;
+ }
+
+ &__purple-bold-border {
+ background-color: $ui-vibrant-blue-10;
+ border-color: $core-vibrant-blue;
+ }
+
&__yellow {
background-color: $ui-yellow-banner;
border-color: $ui-yellow-banner-outline;
diff --git a/frontend/components/buttons/Button/_styles.scss b/frontend/components/buttons/Button/_styles.scss
index cd32c04e66..0b91ab4b7a 100644
--- a/frontend/components/buttons/Button/_styles.scss
+++ b/frontend/components/buttons/Button/_styles.scss
@@ -341,9 +341,7 @@ $base-class: "button";
}
&--disabled {
- opacity: 0.5;
- filter: grayscale(0.5);
- cursor: default;
+ @include disabled;
&:hover,
&:focus {
diff --git a/frontend/components/icons/Chevron.tsx b/frontend/components/icons/Chevron.tsx
index d5e17a7a8e..c9f142e449 100644
--- a/frontend/components/icons/Chevron.tsx
+++ b/frontend/components/icons/Chevron.tsx
@@ -1,10 +1,12 @@
import React from "react";
import { COLORS, Colors } from "styles/var/colors";
+import { IconSizes, ICON_SIZES } from "styles/var/icon_sizes";
interface IChevronProps {
color?: Colors;
/** Default direction "down" */
direction?: "up" | "down" | "left" | "right";
+ size: IconSizes;
}
const SVG_PATH = {
@@ -17,11 +19,12 @@ const SVG_PATH = {
const Chevron = ({
color = "core-fleet-black",
direction = "down",
+ size = "medium",
}: IChevronProps) => {
return (