From dea1d6b66ec9a91cbe6e34fd480c714b1c92cb54 Mon Sep 17 00:00:00 2001 From: Luke Heath Date: Wed, 4 Jan 2023 10:06:30 -0600 Subject: [PATCH 1/6] UI hackathon transitions (#9163) --- frontend/components/MainContent/_styles.scss | 1 + frontend/components/Modal/_styles.scss | 2 + frontend/components/Spinner/_styles.scss | 2 +- .../TableContainer/DataTable/_styles.scss | 17 +++++--- .../buttons/DropdownButton/_styles.scss | 2 +- .../forms/fields/Dropdown/_styles.scss | 1 + .../components/OrgSettingsForm/_styles.scss | 1 + .../components/LabelFilterSelect/_styles.scss | 1 + .../AddPolicyModal/AddPolicyModal.tsx | 12 +++--- .../components/AddPolicyModal/_styles.scss | 11 +++++ frontend/styles/global/_animations.scss | 40 +++++++++++++++++++ frontend/styles/var/colors.scss | 1 + 12 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 frontend/styles/global/_animations.scss diff --git a/frontend/components/MainContent/_styles.scss b/frontend/components/MainContent/_styles.scss index 456521cf5a..a77e517818 100644 --- a/frontend/components/MainContent/_styles.scss +++ b/frontend/components/MainContent/_styles.scss @@ -6,4 +6,5 @@ // of the main-content when there is a sidebar. // Without it the main content pushes the sidebar off the page. overflow: auto; + animation: fade-in 250ms ease-out; } diff --git a/frontend/components/Modal/_styles.scss b/frontend/components/Modal/_styles.scss index b1fb6d233c..77fb627ee9 100644 --- a/frontend/components/Modal/_styles.scss +++ b/frontend/components/Modal/_styles.scss @@ -6,6 +6,7 @@ overflow: scroll; display: flex; justify-content: center; + animation: fade-in 150ms ease-out; } &__content { @@ -69,5 +70,6 @@ width: 570px; padding: $pad-xxlarge; border-radius: 8px; + animation: scale-up 150ms ease-out; } } diff --git a/frontend/components/Spinner/_styles.scss b/frontend/components/Spinner/_styles.scss index f50b74ef5e..6af3638d4c 100644 --- a/frontend/components/Spinner/_styles.scss +++ b/frontend/components/Spinner/_styles.scss @@ -2,7 +2,7 @@ display: flex; align-items: center; justify-content: center; - + animation: fade-and-scale 150ms ease-out; &.centered { margin: 120px auto; } diff --git a/frontend/components/TableContainer/DataTable/_styles.scss b/frontend/components/TableContainer/DataTable/_styles.scss index d0fa604734..3d2da37f70 100644 --- a/frontend/components/TableContainer/DataTable/_styles.scss +++ b/frontend/components/TableContainer/DataTable/_styles.scss @@ -55,13 +55,20 @@ $shadow-transition-width: 10px; white-space: initial; // wraps long text with tooltip } - tr:hover { - background-color: $ui-off-white-opaque; // opaque needed for horizontal scroll shadow + tr, .single-row { + transition: background-color 150ms ease-out; + &:hover { + background-color: $ui-off-white-opaque; // opaque needed for horizontal scroll shadow + } } - .single-row:hover { - cursor: pointer; - background-color: $ui-vibrant-blue-10-opaque; // opaque needed for horizontal scroll shadow + .single-row { + &:hover { + cursor: pointer; + } + &:active { + background-color: $ui-vibrant-blue-10-opaque; // opaque needed for horizontal scroll shadow + } } } diff --git a/frontend/components/buttons/DropdownButton/_styles.scss b/frontend/components/buttons/DropdownButton/_styles.scss index 6e8946d85f..548762a629 100644 --- a/frontend/components/buttons/DropdownButton/_styles.scss +++ b/frontend/components/buttons/DropdownButton/_styles.scss @@ -22,7 +22,7 @@ border-radius: 2px; background-color: $core-white; box-shadow: 0 4px 10px rgba(52, 59, 96, 0.15); - + animation: fade-in 150ms ease-out; &--opened { display: inline-block; } diff --git a/frontend/components/forms/fields/Dropdown/_styles.scss b/frontend/components/forms/fields/Dropdown/_styles.scss index d089de8086..f25c4caa90 100644 --- a/frontend/components/forms/fields/Dropdown/_styles.scss +++ b/frontend/components/forms/fields/Dropdown/_styles.scss @@ -188,6 +188,7 @@ border: 0; margin: 1px 0 0; padding: $pad-small; + animation: fade-in 150ms ease-out; } .Select-noresults { diff --git a/frontend/pages/admin/AppSettingsPage/components/OrgSettingsForm/_styles.scss b/frontend/pages/admin/AppSettingsPage/components/OrgSettingsForm/_styles.scss index f79fd51f3f..106c0cf905 100644 --- a/frontend/pages/admin/AppSettingsPage/components/OrgSettingsForm/_styles.scss +++ b/frontend/pages/admin/AppSettingsPage/components/OrgSettingsForm/_styles.scss @@ -72,6 +72,7 @@ &__section { @include clearfix; margin: 0 0 $pad-large; + animation: fade-in 200ms ease-out; .upcaret::after { content: url("../assets/images/icon-collapse-black-16x16@2x.png"); diff --git a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/_styles.scss index 5c64c67658..fbb8a4bdda 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/_styles.scss @@ -67,6 +67,7 @@ width: 300px; margin-top: 0; z-index: 2; + animation: fade-in 150ms ease-out; } .label-filter-select__menu-list { diff --git a/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/AddPolicyModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/AddPolicyModal.tsx index 26fbf64047..c283f3e1be 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/AddPolicyModal.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/AddPolicyModal.tsx @@ -76,11 +76,13 @@ const AddPolicyModal = ({ return ( <> - Choose a policy template to get started or{" "} - - . +
+ Choose a policy template to get started or{" "} + + . +
{policiesAvailable}
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss index c947eccb17..983d6cd1f4 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/components/AddPolicyModal/_styles.scss @@ -1,5 +1,16 @@ .add-policy-modal { + height: 80%; + overflow: hidden; + .modal__content { + height: 100%; + overflow: scroll; + margin-top: 0; + } + &__create-policy { + padding-top: 1.5rem; + } &__policy-selection { padding: $pad-large 0; + height: 100%; } } diff --git a/frontend/styles/global/_animations.scss b/frontend/styles/global/_animations.scss new file mode 100644 index 0000000000..f440c434ba --- /dev/null +++ b/frontend/styles/global/_animations.scss @@ -0,0 +1,40 @@ +@keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fade-and-scale { + from { + opacity: 0; + transform: scale(0); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes scale-up { + from { + transform: scale(0.75); + } + to { + transform: scale(1); + } +} + +// Page transition animation +@keyframes page-transition { + from { + opacity: 0; + transform: translateX(-10px); + } + to { + opacity: 1; + transform: translateX(0); + } +} \ No newline at end of file diff --git a/frontend/styles/var/colors.scss b/frontend/styles/var/colors.scss index b10b7ad865..68e5ab694a 100644 --- a/frontend/styles/var/colors.scss +++ b/frontend/styles/var/colors.scss @@ -12,6 +12,7 @@ $ui-fleet-black-75: #515774; $ui-fleet-black-50: #8b8fa2; $ui-fleet-black-25: #c5c7d1; $ui-fleet-blue-15: #e2e4ea; +$ui-fleet-blue-10: #F9FAFC; $ui-dark-blue-gray: #afbec1; $ui-blue-gray: #dbe3e5; $ui-gray: #e3e3e3; From 7ec3cfbfe10c93eae8f97fcda34ae57a5a4057f5 Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Wed, 4 Jan 2023 16:41:15 +0000 Subject: [PATCH 2/6] add bookmarkability for search query filtering on hosts (#9067) (#9182) --- changes/ui-hackathon-filtering | 1 + frontend/components/TableContainer/TableContainer.tsx | 6 +++++- .../components/forms/fields/SearchField/SearchField.tsx | 4 +++- frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 changes/ui-hackathon-filtering diff --git a/changes/ui-hackathon-filtering b/changes/ui-hackathon-filtering new file mode 100644 index 0000000000..943d15f944 --- /dev/null +++ b/changes/ui-hackathon-filtering @@ -0,0 +1 @@ +- adds bookmarkability of url when it includes the `query` query param on the manage hosts page. diff --git a/frontend/components/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx index 9099a172a4..5ee4cbe5de 100644 --- a/frontend/components/TableContainer/TableContainer.tsx +++ b/frontend/components/TableContainer/TableContainer.tsx @@ -34,6 +34,7 @@ interface ITableContainerProps { manualSortBy?: boolean; defaultSortHeader?: string; defaultSortDirection?: string; + defaultSearchQuery?: string; actionButtonText?: string; actionButtonIcon?: string; actionButtonVariant?: ButtonVariant; @@ -97,6 +98,7 @@ const TableContainer = ({ filters, isLoading, manualSortBy = false, + defaultSearchQuery = "", defaultSortHeader = "name", defaultSortDirection = "asc", inputPlaceHolder = "Search", @@ -143,7 +145,7 @@ const TableContainer = ({ setExportRows, resetPageIndex, }: ITableContainerProps): JSX.Element => { - const [searchQuery, setSearchQuery] = useState(""); + const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); const [sortHeader, setSortHeader] = useState(defaultSortHeader || ""); const [sortDirection, setSortDirection] = useState( defaultSortDirection || "" @@ -269,6 +271,7 @@ const TableContainer = ({
@@ -350,6 +353,7 @@ const TableContainer = ({ > diff --git a/frontend/components/forms/fields/SearchField/SearchField.tsx b/frontend/components/forms/fields/SearchField/SearchField.tsx index 2eaf1697cf..27d8c62d69 100644 --- a/frontend/components/forms/fields/SearchField/SearchField.tsx +++ b/frontend/components/forms/fields/SearchField/SearchField.tsx @@ -7,14 +7,16 @@ const baseClass = "search-field"; export interface ISearchFieldProps { placeholder: string; + defaultValue?: string; onChange: (value: string) => void; } const SearchField = ({ placeholder, + defaultValue = "", onChange, }: ISearchFieldProps): JSX.Element => { - const [searchQueryInput, setSearchQueryInput] = useState(""); + const [searchQueryInput, setSearchQueryInput] = useState(defaultValue); const debouncedOnChange = useDebouncedCallback((newValue: string) => { onChange(newValue); diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index b9ce68d5e5..a23b395047 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -1716,6 +1716,7 @@ const ManageHostsPage = ({ defaultSortDirection={ (sortBy[0] && sortBy[0].direction) || DEFAULT_SORT_DIRECTION } + defaultSearchQuery={searchQuery} pageSize={100} actionButtonText={"Edit columns"} actionButtonIcon={EditColumnsIcon} From 68aefc8e560b735d5018d5ef8de67ab1797d9279 Mon Sep 17 00:00:00 2001 From: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Date: Wed, 4 Jan 2023 14:16:34 -0500 Subject: [PATCH 3/6] Fleet UI Hackathon: Empty states (#9094) --- assets/images/no-matching-host-100x100@2x.png | Bin 1864 -> 0 bytes assets/images/no-policy-323x138@2x.png | Bin 16161 -> 0 bytes assets/images/no-policy.svg | 43 ----- assets/images/no-schedule-322x138@2x.png | Bin 15799 -> 0 bytes assets/images/no-schedule.svg | 41 ----- assets/images/robo-dog-176x144@2x.png | Bin 44322 -> 0 bytes changes/issue-6799-software-empty-states | 1 + .../integration/all/app/policiesflow.spec.ts | 2 +- cypress/integration/pages/dashboardPage.ts | 6 +- cypress/integration/pages/manageHostsPage.ts | 3 +- .../integration/pages/manageSchedulePage.ts | 4 +- cypress/integration/premium/teamflow.spec.ts | 6 +- docs/Using-Fleet/Fleet-UI.md | 4 +- frontend/components/EmptyTable/EmptyTable.tsx | 41 +++++ frontend/components/EmptyTable/_styles.scss | 52 ++++++ frontend/components/EmptyTable/index.ts | 1 + .../TableContainer/TableContainer.tsx | 1 - frontend/components/icons/EmptyHosts.tsx | 173 ++++++++++++++++++ .../components/icons/EmptyIntegrations.tsx | 77 ++++++++ frontend/components/icons/EmptyMembers.tsx | 78 ++++++++ frontend/components/icons/EmptyPacks.tsx | 115 ++++++++++++ frontend/components/icons/EmptyPolicies.tsx | 122 ++++++++++++ frontend/components/icons/EmptyQueries.tsx | 115 ++++++++++++ frontend/components/icons/EmptySchedule.tsx | 115 ++++++++++++ frontend/components/icons/EmptySoftware.tsx | 143 +++++++++++++++ frontend/components/icons/EmptyTeams.tsx | 106 +++++++++++ frontend/components/icons/index.ts | 18 ++ .../EmptySearch/EmptySearch.tsx | 18 -- .../PackQueriesTable/EmptySearch/_styles.scss | 30 --- .../PackQueriesTable/EmptySearch/index.ts | 1 - .../PackQueriesTable/PackQueriesTable.tsx | 9 +- .../queries/PackQueriesTable/_styles.scss | 5 - frontend/interfaces/empty_table.ts | 11 ++ .../pages/DashboardPage/DashboardPage.tsx | 2 +- .../DashboardPage/cards/Software/Software.tsx | 35 ++-- .../IntegrationsPage/IntegrationsPage.tsx | 68 ++++--- .../pages/admin/IntegrationsPage/_styles.scss | 70 ------- .../MembersPage/MembersPage.tsx | 92 +++++----- .../MembersPage/_styles.scss | 72 -------- .../TeamManagementPage/TeamManagementPage.tsx | 71 ++++--- .../admin/TeamManagementPage/_styles.scss | 72 -------- .../admin/UserManagementPage/_styles.scss | 1 + .../components/EmptyUsers/EmptyUsers.tsx | 24 --- .../components/EmptyUsers/_styles.scss | 35 ---- .../components/EmptyUsers/index.ts | 1 - .../components/UsersTable/UsersTable.tsx | 12 +- .../hosts/ManageHostsPage/ManageHostsPage.tsx | 67 ++++++- .../components/EmptyHosts/EmptyHosts.tsx | 32 ---- .../components/EmptyHosts/_styles.scss | 35 ---- .../components/EmptyHosts/index.ts | 1 - .../components/NoHosts/NoHosts.tsx | 70 ------- .../components/NoHosts/_styles.scss | 79 -------- .../components/NoHosts/index.ts | 1 - .../HostDetailsPage/HostDetailsPage.tsx | 4 +- .../hosts/details/cards/Software/Software.tsx | 14 +- .../components/PacksTable/PacksTable.tsx | 72 ++++---- .../components/PacksTable/_styles.scss | 78 -------- .../ManagePoliciesPage/ManagePoliciesPage.tsx | 24 +-- .../PoliciesTable/PoliciesTable.tsx | 117 ++++++------ .../components/PoliciesTable/_styles.scss | 63 ------- .../queries/ManageQueriesPage/_styles.scss | 76 -------- .../components/QueriesTable/QueriesTable.tsx | 112 ++++++------ .../ManageSchedulePage/ManageSchedulePage.tsx | 4 +- .../schedule/ManageSchedulePage/_styles.scss | 105 ----------- .../ScheduleTable/ScheduleTable.tsx | 155 +++++++++------- .../ManageSoftwarePage/ManageSoftwarePage.tsx | 54 +++++- .../software/ManageSoftwarePage/_styles.scss | 25 --- .../software/components/EmptySoftware.tsx | 53 ------ 68 files changed, 1704 insertions(+), 1433 deletions(-) delete mode 100644 assets/images/no-matching-host-100x100@2x.png delete mode 100644 assets/images/no-policy-323x138@2x.png delete mode 100644 assets/images/no-policy.svg delete mode 100644 assets/images/no-schedule-322x138@2x.png delete mode 100644 assets/images/no-schedule.svg delete mode 100644 assets/images/robo-dog-176x144@2x.png create mode 100644 changes/issue-6799-software-empty-states create mode 100644 frontend/components/EmptyTable/EmptyTable.tsx create mode 100644 frontend/components/EmptyTable/_styles.scss create mode 100644 frontend/components/EmptyTable/index.ts create mode 100644 frontend/components/icons/EmptyHosts.tsx create mode 100644 frontend/components/icons/EmptyIntegrations.tsx create mode 100644 frontend/components/icons/EmptyMembers.tsx create mode 100644 frontend/components/icons/EmptyPacks.tsx create mode 100644 frontend/components/icons/EmptyPolicies.tsx create mode 100644 frontend/components/icons/EmptyQueries.tsx create mode 100644 frontend/components/icons/EmptySchedule.tsx create mode 100644 frontend/components/icons/EmptySoftware.tsx create mode 100644 frontend/components/icons/EmptyTeams.tsx delete mode 100644 frontend/components/queries/PackQueriesTable/EmptySearch/EmptySearch.tsx delete mode 100644 frontend/components/queries/PackQueriesTable/EmptySearch/_styles.scss delete mode 100644 frontend/components/queries/PackQueriesTable/EmptySearch/index.ts create mode 100644 frontend/interfaces/empty_table.ts delete mode 100644 frontend/pages/admin/UserManagementPage/components/EmptyUsers/EmptyUsers.tsx delete mode 100644 frontend/pages/admin/UserManagementPage/components/EmptyUsers/_styles.scss delete mode 100644 frontend/pages/admin/UserManagementPage/components/EmptyUsers/index.ts delete mode 100644 frontend/pages/hosts/ManageHostsPage/components/EmptyHosts/EmptyHosts.tsx delete mode 100644 frontend/pages/hosts/ManageHostsPage/components/EmptyHosts/_styles.scss delete mode 100644 frontend/pages/hosts/ManageHostsPage/components/EmptyHosts/index.ts delete mode 100644 frontend/pages/hosts/ManageHostsPage/components/NoHosts/NoHosts.tsx delete mode 100644 frontend/pages/hosts/ManageHostsPage/components/NoHosts/_styles.scss delete mode 100644 frontend/pages/hosts/ManageHostsPage/components/NoHosts/index.ts delete mode 100644 frontend/pages/software/components/EmptySoftware.tsx diff --git a/assets/images/no-matching-host-100x100@2x.png b/assets/images/no-matching-host-100x100@2x.png deleted file mode 100644 index 7d2cac45c94fd74f5d77e3db8613fba58f0f19f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1864 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�Gq+?NW($HIw)|t95e1Ue0 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 k0{k - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/no-schedule-322x138@2x.png b/assets/images/no-schedule-322x138@2x.png deleted file mode 100644 index 3dac4cc4ccf0edda194f901a5402f42ca05dd2e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15799 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/robo-dog-176x144@2x.png b/assets/images/robo-dog-176x144@2x.png deleted file mode 100644 index b2859d3b803fbe07e30b9bd9043bb49707dd26d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44322 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 { managePoliciesPage.visitManagePoliciesPage(); }); it("creates a custom policy", () => { - cy.getAttached(".policies-table__action-button-container").within(() => { + cy.getAttached(".empty-table__cta-buttons").within(() => { cy.findByText(/add a policy/i).click(); }); cy.findByText(/create your own policy/i).click(); diff --git a/cypress/integration/pages/dashboardPage.ts b/cypress/integration/pages/dashboardPage.ts index 992cc143cd..d73cc12a44 100644 --- a/cypress/integration/pages/dashboardPage.ts +++ b/cypress/integration/pages/dashboardPage.ts @@ -66,8 +66,9 @@ const dashboardPage = { cy.getAttached(".dashboard-page__wrapper").within(() => { cy.findByText(/platform/i).should("exist"); cy.getAttached(".hosts-summary").should("exist"); - cy.getAttached(".home-software").should("exist"); cy.getAttached(".activity-feed").should("exist"); + // hidden if no software + cy.get(".home-software").should("not.exist"); if (tier === "premium") { cy.getAttached(".hosts-missing").should("exist"); cy.getAttached(".hosts-low-space").should("exist"); @@ -82,7 +83,8 @@ const dashboardPage = { cy.getAttached(".dashboard-page__wrapper").within(() => { cy.findByText(/platform/i).should("exist"); cy.getAttached(".hosts-summary").should("exist"); - cy.getAttached(".home-software").should("exist"); + // hidden if no software + cy.get(".home-software").should("not.exist"); cy.get(".activity-feed").should("not.exist"); if (tier === "premium") { cy.getAttached(".hosts-missing").should("exist"); diff --git a/cypress/integration/pages/manageHostsPage.ts b/cypress/integration/pages/manageHostsPage.ts index f7b0d9b681..419f575b8a 100644 --- a/cypress/integration/pages/manageHostsPage.ts +++ b/cypress/integration/pages/manageHostsPage.ts @@ -9,7 +9,8 @@ const manageHostsPage = { .click(); cy.contains("button", /add secret/i).click(); cy.contains("button", /save/i).click(); - cy.contains("button", /done/i).click(); + cy.findByText(/successfully added/i); + cy.getAttached(".modal__ex").click(); }, allowsAddHosts: () => { diff --git a/cypress/integration/pages/manageSchedulePage.ts b/cypress/integration/pages/manageSchedulePage.ts index a0ece28351..a9a8c8a35b 100644 --- a/cypress/integration/pages/manageSchedulePage.ts +++ b/cypress/integration/pages/manageSchedulePage.ts @@ -5,7 +5,7 @@ const manageSchedulePage = { hidesButton: (text: string) => { if (text === "Advanced") { - cy.getAttached(".no-schedule__cta-buttons").within(() => { + cy.getAttached(".empty-table__cta-buttons").within(() => { cy.contains("button", text).should("not.exist"); }); } else cy.contains("button", text).should("not.exist"); @@ -25,7 +25,7 @@ const manageSchedulePage = { }, allowsAddSchedule: () => { - cy.getAttached(".no-schedule__cta-buttons").within(() => { + cy.getAttached(".empty-table__cta-buttons").within(() => { cy.findByRole("button", { name: /schedule a query/i }).click({ force: true, }); diff --git a/cypress/integration/premium/teamflow.spec.ts b/cypress/integration/premium/teamflow.spec.ts index 7b7a3e3a2a..b93d49ac45 100644 --- a/cypress/integration/premium/teamflow.spec.ts +++ b/cypress/integration/premium/teamflow.spec.ts @@ -16,10 +16,8 @@ describe("Teams flow (empty)", () => { cy.visit("/settings/teams"); }); it("creates a new team", () => { - cy.getAttached(".no-teams").within(() => { - cy.getAttached(".no-teams__inner-text").within(() => { - cy.contains("button", /create team/i).click(); - }); + cy.getAttached(".empty-table__cta-buttons").within(() => { + cy.contains("button", /create team/i).click(); }); cy.findByLabelText(/team name/i) .click() diff --git a/docs/Using-Fleet/Fleet-UI.md b/docs/Using-Fleet/Fleet-UI.md index 9cc3abbfe7..4dec4d5652 100644 --- a/docs/Using-Fleet/Fleet-UI.md +++ b/docs/Using-Fleet/Fleet-UI.md @@ -60,9 +60,9 @@ How to schedule a query: 5. Select **Schedule**. -With [the teams feature](https://fleetdm.com/docs/using-fleet/teams), you can schedule queries for groups of hosts. This allows you to collect different data for each group. +With Fleet Premium, you can schedule queries for groups of hosts using [the teams feature](https://fleetdm.com/docs/using-fleet/teams). This allows you to collect different data for each group. -> In Fleet, groups of hosts are called "teams." +> In Fleet Premium, groups of hosts are called "teams." How to use teams to schedule queries for a group of hosts: diff --git a/frontend/components/EmptyTable/EmptyTable.tsx b/frontend/components/EmptyTable/EmptyTable.tsx new file mode 100644 index 0000000000..d4d12577b2 --- /dev/null +++ b/frontend/components/EmptyTable/EmptyTable.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import classnames from "classnames"; +import Icon from "components/Icon"; +import { IEmptyTableProps } from "interfaces/empty_table"; + +const baseClass = "empty-table"; + +const EmptyTable = ({ + iconName, + header, + info, + additionalInfo, + className, + primaryButton, + secondaryButton, +}: IEmptyTableProps): JSX.Element => { + const emptyTableClass = classnames(`${baseClass}__container`, className); + + return ( +
+ {iconName && ( +
+ +
+ )} +
+ {header &&

{header}

} + {info &&

{info}

} + {additionalInfo &&

{additionalInfo}

} +
+ {primaryButton && ( +
+ {primaryButton} + {secondaryButton && secondaryButton} +
+ )} +
+ ); +}; + +export default EmptyTable; diff --git a/frontend/components/EmptyTable/_styles.scss b/frontend/components/EmptyTable/_styles.scss new file mode 100644 index 0000000000..f025b3b0ae --- /dev/null +++ b/frontend/components/EmptyTable/_styles.scss @@ -0,0 +1,52 @@ +.empty-table { + &__container { + display: flex; + flex-direction: column; + align-items: center; + margin: 96px auto 0; // 96px to top of div + max-width: 450px; // standard empty state width + gap: $pad-medium; // 16px between image, text, and buttons + } + + &__inner { + display: flex; + flex-direction: column; + gap: $pad-small; // 4px from header to info text + + h2, + h2 a { + text-align: center; + font-size: $small; + font-weight: $bold; + margin: 0; + } + + p { + text-align: center; + color: $core-fleet-blue; + font-size: $x-small; + margin: 0; + } + + ul { + margin: 0; + padding: 0; + color: $core-fleet-black; + list-style: none; + + li { + &::before { + content: "•"; + color: $core-vibrant-blue; + margin-right: $pad-medium; + } + } + } + } + + &__cta-buttons { + display: flex; + justify-content: center; + gap: $pad-medium; // 16px between buttons + } +} diff --git a/frontend/components/EmptyTable/index.ts b/frontend/components/EmptyTable/index.ts new file mode 100644 index 0000000000..afaaa562c8 --- /dev/null +++ b/frontend/components/EmptyTable/index.ts @@ -0,0 +1 @@ +export { default } from "./EmptyTable"; diff --git a/frontend/components/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx index 5ee4cbe5de..ae3953eccf 100644 --- a/frontend/components/TableContainer/TableContainer.tsx +++ b/frontend/components/TableContainer/TableContainer.tsx @@ -338,7 +338,6 @@ const TableContainer = ({ {customControl && customControl()} -
{/* Render search bar only if not empty component */} {searchable && !wideSearch && ( diff --git a/frontend/components/icons/EmptyHosts.tsx b/frontend/components/icons/EmptyHosts.tsx new file mode 100644 index 0000000000..8996f477eb --- /dev/null +++ b/frontend/components/icons/EmptyHosts.tsx @@ -0,0 +1,173 @@ +import React from "react"; + +const EmptyHosts = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default EmptyHosts; diff --git a/frontend/components/icons/EmptyIntegrations.tsx b/frontend/components/icons/EmptyIntegrations.tsx new file mode 100644 index 0000000000..b15b2230fd --- /dev/null +++ b/frontend/components/icons/EmptyIntegrations.tsx @@ -0,0 +1,77 @@ +import React from "react"; + +const EmptyIntegrations = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default EmptyIntegrations; diff --git a/frontend/components/icons/EmptyMembers.tsx b/frontend/components/icons/EmptyMembers.tsx new file mode 100644 index 0000000000..b4c6328fe6 --- /dev/null +++ b/frontend/components/icons/EmptyMembers.tsx @@ -0,0 +1,78 @@ +import React from "react"; + +const EmptyMembers = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default EmptyMembers; diff --git a/frontend/components/icons/EmptyPacks.tsx b/frontend/components/icons/EmptyPacks.tsx new file mode 100644 index 0000000000..2d949841ea --- /dev/null +++ b/frontend/components/icons/EmptyPacks.tsx @@ -0,0 +1,115 @@ +import React from "react"; + +const EmptyPacks = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default EmptyPacks; diff --git a/frontend/components/icons/EmptyPolicies.tsx b/frontend/components/icons/EmptyPolicies.tsx new file mode 100644 index 0000000000..27b8594167 --- /dev/null +++ b/frontend/components/icons/EmptyPolicies.tsx @@ -0,0 +1,122 @@ +import React from "react"; + +const EmptyPolicies = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default EmptyPolicies; diff --git a/frontend/components/icons/EmptyQueries.tsx b/frontend/components/icons/EmptyQueries.tsx new file mode 100644 index 0000000000..9062a89f3e --- /dev/null +++ b/frontend/components/icons/EmptyQueries.tsx @@ -0,0 +1,115 @@ +import React from "react"; + +const EmptyQuery = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default EmptyQuery; diff --git a/frontend/components/icons/EmptySchedule.tsx b/frontend/components/icons/EmptySchedule.tsx new file mode 100644 index 0000000000..68e4ef13b5 --- /dev/null +++ b/frontend/components/icons/EmptySchedule.tsx @@ -0,0 +1,115 @@ +import React from "react"; + +const EmptySchedule = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default EmptySchedule; diff --git a/frontend/components/icons/EmptySoftware.tsx b/frontend/components/icons/EmptySoftware.tsx new file mode 100644 index 0000000000..b2488c178e --- /dev/null +++ b/frontend/components/icons/EmptySoftware.tsx @@ -0,0 +1,143 @@ +import React from "react"; + +const EmptySoftware = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default EmptySoftware; diff --git a/frontend/components/icons/EmptyTeams.tsx b/frontend/components/icons/EmptyTeams.tsx new file mode 100644 index 0000000000..226609bb98 --- /dev/null +++ b/frontend/components/icons/EmptyTeams.tsx @@ -0,0 +1,106 @@ +import React from "react"; + +const EmptyTeams = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default EmptyTeams; diff --git a/frontend/components/icons/index.ts b/frontend/components/icons/index.ts index 1a1673c2da..24e710ccea 100644 --- a/frontend/components/icons/index.ts +++ b/frontend/components/icons/index.ts @@ -3,6 +3,15 @@ import CalendarCheck from "./CalendarCheck"; import Check from "./Check"; import Chevron from "./Chevron"; import Ex from "./Ex"; +import EmptyHosts from "./EmptyHosts"; +import EmptyIntegrations from "./EmptyIntegrations"; +import EmptyMembers from "./EmptyMembers"; +import EmptyPacks from "./EmptyPacks"; +import EmptyPolicies from "./EmptyPolicies"; +import EmptyQueries from "./EmptyQueries"; +import EmptySchedule from "./EmptySchedule"; +import EmptySoftware from "./EmptySoftware"; +import EmptyTeams from "./EmptyTeams"; import ExternalLink from "./ExternalLink"; import Issue from "./Issue"; import Plus from "./Plus"; @@ -37,6 +46,15 @@ export const ICON_MAP = { chevron: Chevron, check: Check, ex: Ex, + "empty-hosts": EmptyHosts, + "empty-integrations": EmptyIntegrations, + "empty-members": EmptyMembers, + "empty-packs": EmptyPacks, + "empty-policies": EmptyPolicies, + "empty-queries": EmptyQueries, + "empty-schedule": EmptySchedule, + "empty-software": EmptySoftware, + "empty-teams": EmptyTeams, "external-link": ExternalLink, "low-disk-space-hosts": LowDiskSpaceHosts, "missing-hosts": MissingHosts, diff --git a/frontend/components/queries/PackQueriesTable/EmptySearch/EmptySearch.tsx b/frontend/components/queries/PackQueriesTable/EmptySearch/EmptySearch.tsx deleted file mode 100644 index be1f01f3dd..0000000000 --- a/frontend/components/queries/PackQueriesTable/EmptySearch/EmptySearch.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; - -const baseClass = "empty-pack"; - -const EmptyPack = (): JSX.Element => { - return ( -
-
-
-

No queries matched your search criteria.

-

Try a different search.

-
-
-
- ); -}; - -export default EmptyPack; diff --git a/frontend/components/queries/PackQueriesTable/EmptySearch/_styles.scss b/frontend/components/queries/PackQueriesTable/EmptySearch/_styles.scss deleted file mode 100644 index cc7344c6e7..0000000000 --- a/frontend/components/queries/PackQueriesTable/EmptySearch/_styles.scss +++ /dev/null @@ -1,30 +0,0 @@ -.empty-pack { - display: flex; - flex-direction: column; - align-items: center; - margin-top: 80px; - - &__inner { - display: flex; - flex-direction: row; - - h1 { - font-size: $small; - font-weight: $bold; - margin-bottom: $pad-medium; - } - - p { - color: $core-fleet-black; - font-weight: $regular; - font-size: $x-small; - margin: 0; - } - } - - &__empty-filter-results { - display: flex; - flex-direction: column; - width: 350px; - } -} diff --git a/frontend/components/queries/PackQueriesTable/EmptySearch/index.ts b/frontend/components/queries/PackQueriesTable/EmptySearch/index.ts deleted file mode 100644 index 1a4053f224..0000000000 --- a/frontend/components/queries/PackQueriesTable/EmptySearch/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./EmptySearch"; diff --git a/frontend/components/queries/PackQueriesTable/PackQueriesTable.tsx b/frontend/components/queries/PackQueriesTable/PackQueriesTable.tsx index 8fdda48077..e9dd70aa8c 100644 --- a/frontend/components/queries/PackQueriesTable/PackQueriesTable.tsx +++ b/frontend/components/queries/PackQueriesTable/PackQueriesTable.tsx @@ -5,7 +5,7 @@ import { IScheduledQuery } from "interfaces/scheduled_query"; import TableContainer, { ITableQueryData } from "components/TableContainer"; import Button from "components/buttons/Button"; -import EmptySearch from "./EmptySearch"; +import EmptyTable from "components/EmptyTable"; import { generateTableHeaders, generateDataSet, @@ -82,7 +82,12 @@ const PackQueriesTable = ({ inputPlaceHolder={"Search queries"} onQueryChange={onTableQueryChange} resultsTitle={"queries"} - emptyComponent={EmptySearch} + emptyComponent={() => + EmptyTable({ + header: "No queries match your search criteria.", + info: "Try a different search.", + }) + } showMarkAllPages={false} actionButtonText={"Add query"} actionButtonIcon={AddQueryIcon} diff --git a/frontend/components/queries/PackQueriesTable/_styles.scss b/frontend/components/queries/PackQueriesTable/_styles.scss index ef22ec93a8..8c79e0ec9b 100644 --- a/frontend/components/queries/PackQueriesTable/_styles.scss +++ b/frontend/components/queries/PackQueriesTable/_styles.scss @@ -92,9 +92,4 @@ color: $core-fleet-black; } } - - &__no-queries { - font-size: $x-small; - font-weight: $bold; - } } diff --git a/frontend/interfaces/empty_table.ts b/frontend/interfaces/empty_table.ts new file mode 100644 index 0000000000..931179f782 --- /dev/null +++ b/frontend/interfaces/empty_table.ts @@ -0,0 +1,11 @@ +import { IconNames } from "components/icons"; + +export interface IEmptyTableProps { + iconName?: IconNames; + header?: JSX.Element | string; + info?: JSX.Element | string; + additionalInfo?: JSX.Element | string; + className?: string; + primaryButton?: JSX.Element; + secondaryButton?: JSX.Element; +} diff --git a/frontend/pages/DashboardPage/DashboardPage.tsx b/frontend/pages/DashboardPage/DashboardPage.tsx index cabe7b4c2e..f289449fda 100644 --- a/frontend/pages/DashboardPage/DashboardPage.tsx +++ b/frontend/pages/DashboardPage/DashboardPage.tsx @@ -590,7 +590,7 @@ const DashboardPage = ({ {LearnFleetCard} )} - {SoftwareCard} + {!software && SoftwareCard} {!currentTeam && isOnGlobalTeam && <>{ActivityFeedCard}} {showMdmCard && <>{MDMCard}}
diff --git a/frontend/pages/DashboardPage/cards/Software/Software.tsx b/frontend/pages/DashboardPage/cards/Software/Software.tsx index d014f3f57a..64fd1c205a 100644 --- a/frontend/pages/DashboardPage/cards/Software/Software.tsx +++ b/frontend/pages/DashboardPage/cards/Software/Software.tsx @@ -10,9 +10,10 @@ import TabsWrapper from "components/TabsWrapper"; import TableContainer from "components/TableContainer"; import TableDataError from "components/DataError"; import Spinner from "components/Spinner"; +import EmptyTable from "components/EmptyTable"; +import { IEmptyTableProps } from "interfaces/empty_table"; import generateTableHeaders from "./SoftwareTableConfig"; -import EmptySoftware from "../../../software/components/EmptySoftware"; interface ISoftwareCardProps { errorSoftware: Error | null; @@ -62,6 +63,20 @@ const Software = ({ router.push(path); }; + const emptyState = (vuln = false) => { + const emptySoftware: IEmptyTableProps = { + header: "No software detected", + info: + "This report is updated every hour to protect the performance of your devices.", + }; + if (vuln) { + emptySoftware.header = "No vulnerable software detected"; + emptySoftware.info = + "This report is updated every hour to protect the performance of your devices."; + } + return emptySoftware; + }; + // Renders opaque information as host information is loading const opacity = isSoftwareFetching ? { opacity: 0 } : { opacity: 1 }; @@ -92,11 +107,10 @@ const Software = ({ hideActionButton resultsTitle={"software"} emptyComponent={() => - EmptySoftware( - (!isSoftwareEnabled && "disabled") || - (isCollectingInventory && "collecting") || - "default" - ) + EmptyTable({ + header: emptyState().header, + info: emptyState().info, + }) } showMarkAllPages={false} isAllPagesSelected={false} @@ -122,11 +136,10 @@ const Software = ({ hideActionButton resultsTitle={"software"} emptyComponent={() => - EmptySoftware( - (!isSoftwareEnabled && "disabled") || - (isCollectingInventory && "collecting") || - "default" - ) + EmptyTable({ + header: emptyState().header, + info: emptyState().info, + }) } showMarkAllPages={false} isAllPagesSelected={false} diff --git a/frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx b/frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx index 8ef1db04da..4db31377a5 100644 --- a/frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx +++ b/frontend/pages/admin/IntegrationsPage/IntegrationsPage.tsx @@ -12,6 +12,7 @@ import { IIntegrations, } from "interfaces/integration"; import { IApiError } from "interfaces/errors"; +import { IEmptyTableProps } from "interfaces/empty_table"; import Button from "components/buttons/Button"; // @ts-ignore @@ -20,6 +21,7 @@ import configAPI from "services/entities/config"; import TableContainer from "components/TableContainer"; import TableDataError from "components/DataError"; +import EmptyTable from "components/EmptyTable"; import CustomLink from "components/CustomLink"; import AddIntegrationModal from "./components/AddIntegrationModal"; @@ -361,35 +363,33 @@ const IntegrationsPage = (): JSX.Element => { } }; - const NoIntegrationsComponent = () => { - return ( -
-
-
-

Set up integrations

-

- Create tickets automatically when Fleet detects new - vulnerabilities. -

-

- Want to learn more?  - -

- -
-
-
- ); + const emptyState = () => { + const emptyIntegrations: IEmptyTableProps = { + iconName: "empty-integrations", + header: "Set up integrations", + info: + "Create tickets automatically when Fleet detects new software vulnerabilities or hosts failing policies.", + additionalInfo: ( + <> + Want to learn more?  + + + ), + primaryButton: ( + + ), + }; + return emptyIntegrations; }; const tableHeaders = generateTableHeaders(onActionSelection); @@ -416,7 +416,15 @@ const IntegrationsPage = (): JSX.Element => { actionButtonVariant={"brand"} onActionButtonClick={toggleAddIntegrationModal} resultsTitle={"integrations"} - emptyComponent={NoIntegrationsComponent} + emptyComponent={() => + EmptyTable({ + iconName: emptyState().iconName, + header: emptyState().header, + info: emptyState().info, + additionalInfo: emptyState().additionalInfo, + primaryButton: emptyState().primaryButton, + }) + } showMarkAllPages={false} isAllPagesSelected={false} disablePagination diff --git a/frontend/pages/admin/IntegrationsPage/_styles.scss b/frontend/pages/admin/IntegrationsPage/_styles.scss index 8604a7bcde..c1537d0247 100644 --- a/frontend/pages/admin/IntegrationsPage/_styles.scss +++ b/frontend/pages/admin/IntegrationsPage/_styles.scss @@ -8,76 +8,6 @@ } } -.no-integrations { - display: flex; - flex-direction: column; - align-items: center; - margin-top: $pad-xxxlarge; - - h1 { - font-size: $large; - font-weight: $regular; - line-height: normal; - letter-spacing: normal; - color: $core-fleet-black; - } - - h2 { - font-size: $small; - font-weight: $bold; - margin: 0 0 $pad-large; - } - - ul { - margin: 0; - padding: 0; - color: $core-fleet-black; - list-style: none; - - li { - &::before { - content: "•"; - color: $core-vibrant-blue; - margin-right: $pad-medium; - } - } - } - - &__inner { - display: flex; - flex-direction: row; - - h1 { - font-size: $small; - font-weight: $bold; - margin-bottom: $pad-medium; - } - - img { - width: 176px; - margin-right: $pad-xlarge; - } - - p { - color: $core-fleet-black; - font-weight: $regular; - font-size: $x-small; - margin: 0; - margin-bottom: $pad-large; - } - - .no-filter-results { - display: flex; - flex-direction: column; - width: 350px; - } - } - - &__inner-text { - width: 350px; - } -} - .type__header { width: 12px; } diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx index 64529f9c2d..9765445434 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/MembersPage.tsx @@ -1,11 +1,13 @@ import React, { useCallback, useContext, useState } from "react"; import { useQuery } from "react-query"; +import { IconNames } from "components/icons"; import { NotificationContext } from "context/notification"; import PATHS from "router/paths"; import { IApiError } from "interfaces/errors"; import { IUser, IUserFormErrors } from "interfaces/user"; import { INewMembersBody, ITeam } from "interfaces/team"; +import { IEmptyTableProps } from "interfaces/empty_table"; import { Link } from "react-router"; import { AppContext } from "context/app"; import usersAPI from "services/entities/users"; @@ -15,6 +17,7 @@ import teamsAPI, { ILoadTeamsResponse } from "services/entities/teams"; import Button from "components/buttons/Button"; import TableContainer from "components/TableContainer"; import TableDataError from "components/DataError"; +import EmptyTable from "components/EmptyTable"; import CreateUserModal from "pages/admin/UserManagementPage/components/CreateUserModal"; import { DEFAULT_CREATE_USER_ERRORS } from "utilities/constants"; import EditUserModal from "../../../UserManagementPage/components/EditUserModal"; @@ -339,51 +342,41 @@ const MembersPage = ({ } }; - const NoMembersComponent = useCallback(() => { - return ( -
-
-
- {searchString === "" ? ( - <> -

This team doesn't have any members yet.

-

- Expecting to see new team members listed here? Try again in a - few seconds as the system catches up. -

- {isGlobalAdmin && ( - - )} - {isTeamAdmin && ( - - )} - - ) : ( - <> -

We couldn’t find any members.

-

- Expecting to see members? Try again in a few seconds as the - system catches up. -

- - )} -
-
-
- ); - }, [searchString, toggleAddUserModal]); + const emptyState = () => { + const emptyMembers: IEmptyTableProps = { + iconName: "empty-members", + header: "This team doesn't have any members yet.", + info: + "Expecting to see new team members listed here? Try again in a few seconds as the system catches up.", + }; + if (searchString !== "") { + delete emptyMembers.iconName; + emptyMembers.header = "We couldn’t find any members."; + emptyMembers.info = + "Expecting to see members? Try again in a few seconds as the system catches up."; + } else if (isGlobalAdmin) { + emptyMembers.primaryButton = ( + + ); + } else if (isTeamAdmin) { + emptyMembers.primaryButton = ( + + ); + } + return emptyMembers; + }; const tableHeaders = generateTableHeaders(onActionSelection); @@ -417,7 +410,14 @@ const MembersPage = ({ hideActionButton={memberIds.length === 0 && searchString === ""} onQueryChange={({ searchQuery }) => setSearchString(searchQuery)} inputPlaceHolder={"Search"} - emptyComponent={NoMembersComponent} + emptyComponent={() => + EmptyTable({ + iconName: emptyState().iconName, + header: emptyState().header, + info: emptyState().info, + primaryButton: emptyState().primaryButton, + }) + } showMarkAllPages={false} isAllPagesSelected={false} searchable={memberIds.length > 0 || searchString !== ""} diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/_styles.scss b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/_styles.scss index 3316cc31c9..806be61f4e 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/_styles.scss +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/_styles.scss @@ -45,75 +45,3 @@ } } } - -.no-members { - display: flex; - flex-direction: column; - align-items: center; - margin-top: $pad-xxxlarge; - - h1 { - font-size: $large; - font-weight: $regular; - line-height: normal; - letter-spacing: normal; - color: $core-fleet-black; - } - - h2 { - font-size: $small; - font-weight: $bold; - margin: 0 0 $pad-large; - line-height: 20px; - color: $core-fleet-black; - } - - ul { - margin: 0; - padding: 0; - color: $core-fleet-black; - list-style: none; - - li { - &::before { - content: "•"; - color: $core-vibrant-blue; - margin-right: $pad-medium; - } - } - } - - &__inner { - display: flex; - flex-direction: row; - - h1 { - font-size: $small; - font-weight: $bold; - margin-bottom: $pad-medium; - } - - img { - width: 176px; - margin-right: $pad-xlarge; - } - - p { - color: $core-fleet-black; - font-weight: $regular; - font-size: $x-small; - margin: 0; - margin-bottom: $pad-large; - } - - .no-filter-results { - display: flex; - flex-direction: column; - width: 350px; - } - } - - &__inner-text { - width: 350px; - } -} diff --git a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx index bfc6eba35b..90da0f2cad 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx +++ b/frontend/pages/admin/TeamManagementPage/TeamManagementPage.tsx @@ -1,4 +1,5 @@ import React, { useState, useCallback, useContext } from "react"; +import { IconNames } from "components/icons"; import { useQuery } from "react-query"; import { useErrorHandler } from "react-error-boundary"; @@ -6,6 +7,7 @@ import { NotificationContext } from "context/notification"; import { AppContext } from "context/app"; import { ITeam } from "interfaces/team"; import { IApiError } from "interfaces/errors"; +import { IEmptyTableProps } from "interfaces/empty_table"; import teamsAPI, { ILoadTeamsResponse, ITeamFormData, @@ -14,6 +16,7 @@ import teamsAPI, { import Button from "components/buttons/Button"; import TableContainer from "components/TableContainer"; import TableDataError from "components/DataError"; +import EmptyTable from "components/EmptyTable"; import CustomLink from "components/CustomLink"; import CreateTeamModal from "./components/CreateTeamModal"; @@ -197,35 +200,35 @@ const TeamManagementPage = (): JSX.Element => { } }; - const NoTeamsComponent = () => { - return ( -
-
-
-

Set up team permissions

-

- Keep your organization organized and efficient by ensuring every - user has the correct access to the right hosts. -

-

- Want to learn more?  - -

- -
-
-
- ); + const emptyState = () => { + const emptyTeams: IEmptyTableProps = { + iconName: "empty-teams", + header: "Set up team permissions", + info: + "Keep your organization organized and efficient by ensuring every user has the correct access to the right hosts.", + additionalInfo: ( + <> + {" "} + Want to learn more?  + + + ), + primaryButton: ( + + ), + }; + + return emptyTeams; }; const tableHeaders = generateTableHeaders(onActionSelection); @@ -252,7 +255,15 @@ const TeamManagementPage = (): JSX.Element => { onActionButtonClick={toggleCreateTeamModal} onQueryChange={onQueryChange} resultsTitle={"teams"} - emptyComponent={NoTeamsComponent} + emptyComponent={() => + EmptyTable({ + iconName: "empty-teams", + header: emptyState().header, + info: emptyState().info, + additionalInfo: emptyState().additionalInfo, + primaryButton: emptyState().primaryButton, + }) + } showMarkAllPages={false} isAllPagesSelected={false} searchable={teams && teams.length > 0 && searchString !== ""} diff --git a/frontend/pages/admin/TeamManagementPage/_styles.scss b/frontend/pages/admin/TeamManagementPage/_styles.scss index 0fb34c1fa5..f8b12eac5b 100644 --- a/frontend/pages/admin/TeamManagementPage/_styles.scss +++ b/frontend/pages/admin/TeamManagementPage/_styles.scss @@ -38,75 +38,3 @@ } } } - -.no-teams { - display: flex; - flex-direction: column; - align-items: center; - margin-top: $pad-xxxlarge; - - h1 { - font-size: $large; - font-weight: $regular; - line-height: normal; - letter-spacing: normal; - color: $core-fleet-black; - } - - h2 { - font-size: $small; - font-weight: $bold; - margin: 0 0 $pad-large; - line-height: 20px; - color: $core-fleet-black; - } - - ul { - margin: 0; - padding: 0; - color: $core-fleet-black; - list-style: none; - - li { - &::before { - content: "•"; - color: $core-vibrant-blue; - margin-right: $pad-medium; - } - } - } - - &__inner { - display: flex; - flex-direction: row; - - h1 { - font-size: $small; - font-weight: $bold; - margin-bottom: $pad-medium; - } - - img { - width: 176px; - margin-right: $pad-xlarge; - } - - p { - color: $core-fleet-black; - font-weight: $regular; - font-size: $x-small; - margin: 0; - margin-bottom: $pad-large; - } - - .no-filter-results { - display: flex; - flex-direction: column; - width: 350px; - } - } - - &__inner-text { - width: 350px; - } -} diff --git a/frontend/pages/admin/UserManagementPage/_styles.scss b/frontend/pages/admin/UserManagementPage/_styles.scss index 46a5063044..b4730838e3 100644 --- a/frontend/pages/admin/UserManagementPage/_styles.scss +++ b/frontend/pages/admin/UserManagementPage/_styles.scss @@ -3,6 +3,7 @@ font-size: $x-small; color: $core-fleet-black; @include sticky-settings-description; + padding-bottom: $pad-medium; } &__sandbox-demo-message { diff --git a/frontend/pages/admin/UserManagementPage/components/EmptyUsers/EmptyUsers.tsx b/frontend/pages/admin/UserManagementPage/components/EmptyUsers/EmptyUsers.tsx deleted file mode 100644 index 05438cca72..0000000000 --- a/frontend/pages/admin/UserManagementPage/components/EmptyUsers/EmptyUsers.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Component when there is no host results found in a search - */ -import React from "react"; - -const baseClass = "empty-users"; - -const EmptyUsers = (): JSX.Element => { - return ( -
-
-
-

No users match the current criteria.

-

- Expecting to see users? Try again in a few seconds as the system - catches up. -

-
-
-
- ); -}; - -export default EmptyUsers; diff --git a/frontend/pages/admin/UserManagementPage/components/EmptyUsers/_styles.scss b/frontend/pages/admin/UserManagementPage/components/EmptyUsers/_styles.scss deleted file mode 100644 index abf7cc4a31..0000000000 --- a/frontend/pages/admin/UserManagementPage/components/EmptyUsers/_styles.scss +++ /dev/null @@ -1,35 +0,0 @@ -.empty-users { - display: flex; - flex-direction: column; - align-items: center; - margin-top: $pad-xxxlarge; - - &__inner { - display: flex; - flex-direction: row; - - h1 { - font-size: $small; - font-weight: $bold; - margin-bottom: $pad-medium; - } - - img { - width: 176px; - margin-right: $pad-xlarge; - } - - p { - color: $core-fleet-black; - font-weight: $regular; - font-size: $x-small; - margin: 0; - } - } - - &__empty-filter-results { - display: flex; - flex-direction: column; - width: 350px; - } -} diff --git a/frontend/pages/admin/UserManagementPage/components/EmptyUsers/index.ts b/frontend/pages/admin/UserManagementPage/components/EmptyUsers/index.ts deleted file mode 100644 index 28df7b7cca..0000000000 --- a/frontend/pages/admin/UserManagementPage/components/EmptyUsers/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./EmptyUsers"; diff --git a/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx b/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx index c294b0fc4a..374931eee6 100644 --- a/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx +++ b/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx @@ -16,11 +16,11 @@ import teamsAPI, { ILoadTeamsResponse } from "services/entities/teams"; import usersAPI from "services/entities/users"; import invitesAPI from "services/entities/invites"; +import { DEFAULT_CREATE_USER_ERRORS } from "utilities/constants"; import TableContainer, { ITableQueryData } from "components/TableContainer"; import TableDataError from "components/DataError"; import Modal from "components/Modal"; -import { DEFAULT_CREATE_USER_ERRORS } from "utilities/constants"; -import EmptyUsers from "../EmptyUsers"; +import EmptyTable from "components/EmptyTable"; import { generateTableHeaders, combineDataSets } from "./UsersTableConfig"; import DeleteUserModal from "../DeleteUserModal"; import ResetPasswordModal from "../ResetPasswordModal"; @@ -520,6 +520,12 @@ const UsersTable = ({ router }: IUsersTableProps): JSX.Element => { tableData = combineUsersAndInvites(users, invites, currentUser?.id); } + const emptyState = { + header: "No users match the current criteria.", + info: + "Expecting to see users? Try again in a few seconds as the system catches up.", + }; + return ( <> {/* TODO: find a way to move these controls into the table component */} @@ -537,7 +543,7 @@ const UsersTable = ({ router }: IUsersTableProps): JSX.Element => { onActionButtonClick={toggleCreateUserModal} onQueryChange={onTableQueryChange} resultsTitle={"users"} - emptyComponent={EmptyUsers} + emptyComponent={() => EmptyTable(emptyState)} searchable showMarkAllPages={false} isAllPagesSelected={false} diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index a23b395047..44c6824e8b 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx @@ -1,4 +1,5 @@ import React, { useState, useContext, useEffect, useCallback } from "react"; +import { IconNames } from "components/icons"; import { useQuery } from "react-query"; import { InjectedRouter, Params } from "react-router/lib/Router"; import { RouteProps } from "react-router/lib/Route"; @@ -42,6 +43,8 @@ import { import { IPolicy, IStoredPolicyResponse } from "interfaces/policy"; import { ISoftware } from "interfaces/software"; import { ITeam } from "interfaces/team"; +import { IEmptyTableProps } from "interfaces/empty_table"; + import sortUtils from "utilities/sort"; import { HOSTS_SEARCH_BOX_PLACEHOLDER, @@ -59,6 +62,7 @@ import { IActionButtonProps } from "components/TableContainer/DataTable/ActionBu import TeamsDropdown from "components/TeamsDropdown"; import Spinner from "components/Spinner"; import MainContent from "components/MainContent"; +import EmptyTable from "components/EmptyTable"; import { getValidatedTeamId } from "utilities/helpers"; import { @@ -78,8 +82,6 @@ import DeleteSecretModal from "../../../components/EnrollSecrets/DeleteSecretMod import SecretEditorModal from "../../../components/EnrollSecrets/SecretEditorModal"; import AddHostsModal from "../../../components/AddHostsModal"; import EnrollSecretModal from "../../../components/EnrollSecrets/EnrollSecretModal"; -import NoHosts from "./components/NoHosts"; -import EmptyHosts from "./components/EmptyHosts"; import PoliciesFilter from "./components/PoliciesFilter"; // @ts-ignore import EditColumnsModal from "./components/EditColumnsModal/EditColumnsModal"; @@ -1679,12 +1681,41 @@ const ManageHostsPage = ({ osVersion ); + const emptyState = () => { + const emptyHosts: IEmptyTableProps = { + iconName: "empty-hosts", + header: "Devices will show up here once they’re added to Fleet.", + info: + "Expecting to see devices? Try again in a few seconds as the system catches up.", + }; + if (includesNameCardFilter) { + delete emptyHosts.iconName; + emptyHosts.header = "No hosts match the current criteria"; + emptyHosts.info = + "Expecting to see new hosts? Try again in a few seconds as the system catches up."; + } + if (canEnrollHosts) { + emptyHosts.header = "Add your devices to Fleet"; + emptyHosts.info = "Generate an installer to add your own devices."; + emptyHosts.primaryButton = ( + + ); + } + return emptyHosts; + }; + return ( - + <> + {EmptyTable({ + iconName: emptyState().iconName, + header: emptyState().header, + info: emptyState().info, + additionalInfo: emptyState().additionalInfo, + primaryButton: emptyState().primaryButton, + })} + ); } @@ -1706,6 +1737,21 @@ const ManageHostsPage = ({ currentTeam ); + const emptyState = () => { + const emptyHosts: IEmptyTableProps = { + header: "No hosts match the current criteria", + info: + "Expecting to see new hosts? Try again in a few seconds as the system catches up.", + }; + if (isLastPage) { + emptyHosts.header = "No more hosts to display"; + emptyHosts.info = + "Expecting to see more hosts? Try again in a few seconds as the system catches up."; + } + + return emptyHosts; + }; + return ( + EmptyTable({ + header: emptyState().header, + info: emptyState().info, + }) + } customControl={renderCustomControls} onActionButtonClick={toggleEditColumnsModal} onPrimarySelectActionClick={onDeleteHostsClick} diff --git a/frontend/pages/hosts/ManageHostsPage/components/EmptyHosts/EmptyHosts.tsx b/frontend/pages/hosts/ManageHostsPage/components/EmptyHosts/EmptyHosts.tsx deleted file mode 100644 index 5cecc1a3b9..0000000000 --- a/frontend/pages/hosts/ManageHostsPage/components/EmptyHosts/EmptyHosts.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Component when there is no host results found in a search - */ -import React from "react"; - -const baseClass = "empty-hosts"; - -interface IEmptyHostsProps { - pageIndex: number; -} - -const EmptyHosts = ({ pageIndex }: IEmptyHostsProps): JSX.Element => { - return ( -
-
-
-

- {pageIndex !== 0 - ? "No more hosts to display" - : "No hosts match the current criteria"} -

-

- Expecting to see {pageIndex !== 0 ? "more" : "new"} hosts? Try again - in a few seconds as the system catches up -

-
-
-
- ); -}; - -export default EmptyHosts; diff --git a/frontend/pages/hosts/ManageHostsPage/components/EmptyHosts/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/EmptyHosts/_styles.scss deleted file mode 100644 index a533a1b8f9..0000000000 --- a/frontend/pages/hosts/ManageHostsPage/components/EmptyHosts/_styles.scss +++ /dev/null @@ -1,35 +0,0 @@ -.empty-hosts { - display: flex; - flex-direction: column; - align-items: center; - margin-top: $pad-xxxlarge; - - &__inner { - display: flex; - flex-direction: row; - - h1 { - font-size: $small; - font-weight: $bold; - margin-bottom: $pad-medium; - } - - img { - width: 176px; - margin-right: $pad-xlarge; - } - - p { - color: $core-fleet-black; - font-weight: $regular; - font-size: $x-small; - margin: 0; - } - } - - &__empty-filter-results { - display: flex; - flex-direction: column; - width: 350px; - } -} diff --git a/frontend/pages/hosts/ManageHostsPage/components/EmptyHosts/index.ts b/frontend/pages/hosts/ManageHostsPage/components/EmptyHosts/index.ts deleted file mode 100644 index 5bbf093b04..0000000000 --- a/frontend/pages/hosts/ManageHostsPage/components/EmptyHosts/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./EmptyHosts"; diff --git a/frontend/pages/hosts/ManageHostsPage/components/NoHosts/NoHosts.tsx b/frontend/pages/hosts/ManageHostsPage/components/NoHosts/NoHosts.tsx deleted file mode 100644 index 690f7fc457..0000000000 --- a/frontend/pages/hosts/ManageHostsPage/components/NoHosts/NoHosts.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Component when there is no hosts set up in fleet - */ -import React from "react"; -import Button from "components/buttons/Button"; - -import RoboDogImage from "../../../../../../assets/images/robo-dog-176x144@2x.png"; - -interface INoHostsProps { - toggleAddHostsModal: () => void; - canEnrollHosts?: boolean; - includesNameCardFilter?: boolean; -} - -const baseClass = "no-hosts"; - -const NoHosts = ({ - toggleAddHostsModal, - canEnrollHosts, - includesNameCardFilter, -}: INoHostsProps): JSX.Element => { - const renderContent = () => { - if (includesNameCardFilter) { - return ( -
-

No hosts match the current criteria

-

- Expecting to see new hosts? Try again in a few seconds as the system - catches up. -

-
- ); - } - - if (canEnrollHosts) { - return ( -
-

Add your devices to Fleet

-

Generate an installer to add your own devices.

-
- -
-
- ); - } - - return ( -
-

Devices will show up here once they’re added to Fleet.

-

- Expecting to see devices? Try again in a few seconds as the system - catches up. -

-
- ); - }; - - return ( -
-
- {!includesNameCardFilter && No Hosts} - {renderContent()} -
-
- ); -}; - -export default NoHosts; diff --git a/frontend/pages/hosts/ManageHostsPage/components/NoHosts/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/NoHosts/_styles.scss deleted file mode 100644 index 332f763b91..0000000000 --- a/frontend/pages/hosts/ManageHostsPage/components/NoHosts/_styles.scss +++ /dev/null @@ -1,79 +0,0 @@ -.no-hosts { - display: flex; - flex-direction: column; - align-items: center; - margin-top: $pad-xxxlarge; - - h1 { - font-size: $large; - font-weight: $regular; - line-height: normal; - letter-spacing: normal; - color: $core-fleet-black; - } - - h2 { - font-size: $x-small; - font-weight: $bold; - margin: 0 0 $pad-large; - line-height: 20px; - color: $core-fleet-black; - } - - ul { - margin: 0; - padding: 0; - color: $core-fleet-black; - list-style: none; - - li { - &::before { - content: "•"; - color: $core-vibrant-blue; - margin-right: $pad-medium; - } - } - } - - p { - color: $core-fleet-black; - font-weight: $regular; - font-size: $x-small; - margin: 0; - } - - &__inner { - display: flex; - flex-direction: row; - - h1 { - font-size: $small; - font-weight: $bold; - margin-bottom: $pad-medium; - } - - img { - width: 176px; - margin-right: $pad-xlarge; - } - - .no-filter-results { - display: flex; - flex-direction: column; - width: 350px; - } - } - - .host-pagination__pager-wrap { - margin-top: $pad-medium; - } - - &__no-hosts-contact { - text-align: left; - margin-top: $pad-large; - } - - &__no-hosts-button { - margin-top: $pad-large; - } -} diff --git a/frontend/pages/hosts/ManageHostsPage/components/NoHosts/index.ts b/frontend/pages/hosts/ManageHostsPage/components/NoHosts/index.ts deleted file mode 100644 index a02c59638c..0000000000 --- a/frontend/pages/hosts/ManageHostsPage/components/NoHosts/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./NoHosts"; diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index b9932e3086..ddf55c0d61 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -682,9 +682,7 @@ const HostDetailsPage = ({ diff --git a/frontend/pages/hosts/details/cards/Software/Software.tsx b/frontend/pages/hosts/details/cards/Software/Software.tsx index 6cfcba05e5..1e5a3dc390 100644 --- a/frontend/pages/hosts/details/cards/Software/Software.tsx +++ b/frontend/pages/hosts/details/cards/Software/Software.tsx @@ -31,7 +31,7 @@ interface ISoftwareTableProps { software: ISoftware[]; deviceUser?: boolean; deviceType?: string; - softwareInventoryEnabled?: boolean; + isSoftwareEnabled?: boolean; router?: InjectedRouter; } @@ -39,6 +39,7 @@ interface IRowProps extends Row { original: { id?: number; }; + isSoftwareEnabled?: boolean; } const SoftwareTable = ({ @@ -46,7 +47,7 @@ const SoftwareTable = ({ software, deviceUser, deviceType, - softwareInventoryEnabled, + isSoftwareEnabled, router, }: ISoftwareTableProps): JSX.Element => { const [searchString, setSearchString] = useState(""); @@ -109,15 +110,6 @@ const SoftwareTable = ({ ); - if (softwareInventoryEnabled === false) { - return ( -
-

Software

- -
- ); - } - return (

Software

diff --git a/frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTable.tsx b/frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTable.tsx index 2510700094..f56d93f973 100644 --- a/frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTable.tsx +++ b/frontend/pages/packs/ManagePacksPage/components/PacksTable/PacksTable.tsx @@ -1,14 +1,15 @@ import React, { useCallback, useEffect, useState } from "react"; import { IPack } from "interfaces/pack"; +import { IEmptyTableProps } from "interfaces/empty_table"; import Button from "components/buttons/Button"; import TableContainer from "components/TableContainer"; +import EmptyTable from "components/EmptyTable"; import { IActionButtonProps } from "components/TableContainer/DataTable/ActionButton"; import { generateTableHeaders, generateDataSet } from "./PacksTableConfig"; const baseClass = "packs-table"; -const noPacksClass = "no-packs"; interface IPacksTableProps { onDeletePackClick: (selectedTablePackIds: number[]) => void; @@ -54,40 +55,32 @@ const PacksTable = ({ [setSearchString] ); - const NoPacksComponent = useCallback(() => { - return ( -
-
-
- {searchString ? ( - <> -

No packs match the current search criteria.

-

- Expecting to see packs? Try again in a few seconds as the - system catches up. -

- - ) : ( - <> -

You don't have any packs

-

- Query packs allow you to schedule recurring queries for your - hosts. -

- - - )} -
-
-
- ); - }, [searchString]); + // TODO: useCallback search string + const emptyState = () => { + const emptyPacks: IEmptyTableProps = { + iconName: "empty-packs", + header: "You don't have any packs", + info: + "Query packs allow you to schedule recurring queries for your hosts.", + primaryButton: ( + + ), + }; + if (searchString) { + delete emptyPacks.iconName; + emptyPacks.header = "No packs match the current search criteria."; + emptyPacks.info = + "Expecting to see packs? Try again in a few seconds as the system catches up."; + delete emptyPacks.primaryButton; + } + return emptyPacks; + }; const tableHeaders = generateTableHeaders(); @@ -127,7 +120,14 @@ const PacksTable = ({ primarySelectActionButtonIcon="delete" primarySelectActionButtonText={"Delete"} secondarySelectActions={secondarySelectActions} - emptyComponent={NoPacksComponent} + emptyComponent={() => + EmptyTable({ + iconName: emptyState().iconName, + header: emptyState().header, + info: emptyState().info, + primaryButton: emptyState().primaryButton, + }) + } />
); diff --git a/frontend/pages/packs/ManagePacksPage/components/PacksTable/_styles.scss b/frontend/pages/packs/ManagePacksPage/components/PacksTable/_styles.scss index 61fe248da6..f9105c710e 100644 --- a/frontend/pages/packs/ManagePacksPage/components/PacksTable/_styles.scss +++ b/frontend/pages/packs/ManagePacksPage/components/PacksTable/_styles.scss @@ -38,82 +38,4 @@ } } } - - &__empty-table { - text-align: center; - font-size: $x-small; - color: $core-fleet-black; - } -} - -.no-packs { - display: flex; - flex-direction: column; - align-items: center; - margin-top: $pad-xxxlarge; - - h1 { - font-size: $large; - font-weight: $regular; - line-height: normal; - letter-spacing: normal; - color: $core-fleet-black; - } - - h2 { - font-size: $small; - font-weight: $bold; - margin: 0 0 $pad-large; - line-height: 20px; - color: $core-fleet-black; - } - - ul { - margin: 0; - padding: 0; - color: $core-fleet-black; - list-style: none; - - li { - &::before { - content: "•"; - color: $core-vibrant-blue; - margin-right: $pad-medium; - } - } - } - - &__inner { - display: flex; - flex-direction: row; - - h1 { - font-size: $small; - font-weight: $bold; - margin-bottom: $pad-medium; - } - - img { - width: 176px; - margin-right: $pad-xlarge; - } - - p { - color: $core-fleet-black; - font-weight: $regular; - font-size: $x-small; - margin: 0; - margin-bottom: $pad-large; - } - - .no-filter-results { - display: flex; - flex-direction: column; - width: 350px; - } - } - - &__inner-text { - width: 350px; - } } diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx index 5e8161cfa3..ac39f89e8d 100644 --- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx @@ -395,17 +395,19 @@ const ManagePolicyPage = ({ Manage automations )} - {canAddOrDeletePolicy && ( -
- -
- )} + {canAddOrDeletePolicy && + ((!!teamId && !isFetchingTeamPolicies) || + !isFetchingGlobalPolicies) && ( +
+ +
+ )} )} diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx index 5a24f19ac9..7af7573f7e 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx @@ -1,19 +1,20 @@ import React, { useContext } from "react"; +import { IconNames } from "components/icons"; import { AppContext } from "context/app"; import { noop } from "lodash"; import paths from "router/paths"; import { IPolicyStats } from "interfaces/policy"; import { ITeamSummary } from "interfaces/team"; +import { IEmptyTableProps } from "interfaces/empty_table"; import Button from "components/buttons/Button"; import Spinner from "components/Spinner"; import TableContainer from "components/TableContainer"; +import EmptyTable from "components/EmptyTable"; import { generateTableHeaders, generateDataSet } from "./PoliciesTableConfig"; -import policyImage from "../../../../../../assets/images/no-policy-323x138@2x.png"; const baseClass = "policies-table"; -const noPoliciesClass = "no-policies"; const TAGGED_TEMPLATES = { hostsByTeamRoute: (teamId: number | undefined | null) => { @@ -46,62 +47,52 @@ const PoliciesTable = ({ const { config } = useContext(AppContext); - const NoPolicies = () => { - return ( -
- ); + const emptyState = () => { + const emptyPolicies: IEmptyTableProps = { + iconName: "empty-policies", + header: ( + <> + Ask yes or no questions about{" "} + all your hosts + + ), + info: ( + <> + - Verify whether or not your hosts have security features turned on. +
- Track your efforts to keep installed software up to date on + your hosts. +
- Provide owners with a list of hosts that still need changes. + + ), + }; + + if (currentTeam) { + emptyPolicies.header = ( + <> + Ask yes or no questions about hosts assigned to{" "} + + {currentTeam.name} + + + ); + } + if (canAddOrDeletePolicy) { + emptyPolicies.primaryButton = ( + + ); + } + + return emptyPolicies; }; return ( @@ -135,7 +126,15 @@ const PoliciesTable = ({ primarySelectActionButtonVariant="text-icon" primarySelectActionButtonIcon="delete" primarySelectActionButtonText={"Delete"} - emptyComponent={NoPolicies} + emptyComponent={() => + EmptyTable({ + iconName: emptyState().iconName, + header: emptyState().header, + info: emptyState().info, + additionalInfo: emptyState().additionalInfo, + primaryButton: emptyState().primaryButton, + }) + } onQueryChange={noop} disableCount={tableType === "inheritedPolicies"} isClientSidePagination diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/_styles.scss index 911c4c78a1..927e2f5fe9 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/_styles.scss @@ -20,12 +20,6 @@ } } - &__empty-table { - text-align: center; - font-size: $x-small; - color: $core-fleet-black; - } - &__action-button-container { display: flex; justify-content: center; @@ -36,63 +30,6 @@ width: 20px; } -.no-policies { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding-top: $pad-xxxlarge; - - h1 { - font-size: $large; - font-weight: $regular; - line-height: normal; - letter-spacing: normal; - color: $core-fleet-black; - } - - h2 { - font-size: $small; - font-weight: $bold; - margin: 0 0 $pad-large; - line-height: 20px; - color: $core-fleet-black; - } - - &__inner { - display: flex; - flex-direction: column; - align-items: center; - - h1 { - font-size: $small; - font-weight: $bold; - margin-bottom: $pad-medium; - } - - img { - width: 322px; - } - - p { - color: $core-fleet-black; - font-weight: $regular; - font-size: $x-small; - margin: 0; - margin-bottom: $pad-large; - } - } - - &__inner-text { - width: 500px; - padding: $pad-xxlarge 0; - } - - &__bullet-text { - text-align: center; - } -} - .no-team-policy { border: 1px solid #e2e4ea; box-sizing: border-box; diff --git a/frontend/pages/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss index 8e03ff69e6..90528ea3bf 100644 --- a/frontend/pages/queries/ManageQueriesPage/_styles.scss +++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss @@ -174,82 +174,6 @@ } } } - - &__empty-table { - text-align: center; - font-size: $x-small; - color: $core-fleet-black; - } - } - - .no-queries { - display: flex; - flex-direction: column; - align-items: center; - padding-top: $pad-xxxlarge; - - h1 { - font-size: $large; - font-weight: $regular; - line-height: normal; - letter-spacing: normal; - color: $core-fleet-black; - } - - h2 { - font-size: $small; - font-weight: $bold; - margin: 0 0 $pad-large; - line-height: 20px; - color: $core-fleet-black; - } - - ul { - margin: 0; - padding: 0; - color: $core-fleet-black; - list-style: none; - - li { - &::before { - content: "•"; - color: $core-vibrant-blue; - margin-right: $pad-medium; - } - } - } - - &__inner-text { - h1 { - font-size: $small; - font-weight: $bold; - margin-bottom: $pad-medium; - } - - p { - color: $core-fleet-black; - font-weight: $regular; - font-size: $x-small; - margin: 0; - margin-bottom: $pad-small; - - &:last-of-type { - margin-bottom: $pad-large; - } - } - - .no-filter-results { - display: flex; - flex-direction: column; - width: 350px; - } - } - - &__none-created { - display: flex; - flex-direction: column; - align-items: center; - } } } } diff --git a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx index 9af20f3c52..125d2f76b4 100644 --- a/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx +++ b/frontend/pages/queries/ManageQueriesPage/components/QueriesTable/QueriesTable.tsx @@ -1,18 +1,20 @@ /* eslint-disable react/prop-types */ -import React, { useCallback, useContext, useState } from "react"; +import React, { useContext, useState } from "react"; import { AppContext } from "context/app"; import { IQuery } from "interfaces/query"; +import { IEmptyTableProps } from "interfaces/empty_table"; import { ITableQueryData } from "components/TableContainer/TableContainer"; import Button from "components/buttons/Button"; import TableContainer from "components/TableContainer"; import CustomLink from "components/CustomLink"; - +import EmptyTable from "components/EmptyTable"; +import Icon from "components/Icon"; import generateTableHeaders from "./QueriesTableConfig"; const baseClass = "queries-table"; -const noQueriesClass = "no-queries"; + interface IQueryTableData extends IQuery { performance: string; platforms: string[]; @@ -43,63 +45,47 @@ const QueriesTable = ({ setSearchString(searchQuery); }; - const NoQueriesComponent = useCallback(() => { - return ( -
-
-
- {searchString ? ( -
-

No queries match the current search criteria.

-

- Expecting to see queries? Try again in a few seconds as the - system catches up. -

-
- ) : ( -
-

You don't have any queries.

-

- A query is a specific question you can ask about your devices. -

- {!isOnlyObserver && ( - <> -

- Create a new query, or{" "} - -

- - - )} -
- )} -
-
-
- ); - }, [searchString, onCreateQueryClick]); + const emptyState = () => { + const emptyQueries: IEmptyTableProps = { + iconName: "empty-queries", + header: "You don't have any queries", + info: "A query is a specific question you can ask about your devices.", + }; + if (searchString) { + delete emptyQueries.iconName; + emptyQueries.header = "No queries match the current search criteria."; + emptyQueries.info = + "Expecting to see queries? Try again in a few seconds as the system catches up."; + } else if (!isOnlyObserver) { + emptyQueries.additionalInfo = ( + <> + Create a new query, or{" "} + + + ); + emptyQueries.primaryButton = ( + + ); + } + + return emptyQueries; + }; const tableHeaders = currentUser && generateTableHeaders(currentUser); - // Queries have not been created - if (!isLoading && queriesList?.length === 0) { - return ( -
- -
- ); - } + const searchable = !(queriesList?.length === 0 && searchString === ""); + console.log("queriesList", queriesList); return tableHeaders && !isLoading ? (
+ EmptyTable({ + iconName: emptyState().iconName, + header: emptyState().header, + info: emptyState().info, + additionalInfo: emptyState().additionalInfo, + primaryButton: emptyState().primaryButton, + }) + } + customControl={searchable ? customControl : undefined} isClientSideFilter searchQueryColumn="name" selectedDropdownFilter={selectedDropdownFilter} diff --git a/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx b/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx index 3c6d2adf04..30244ce76a 100644 --- a/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx +++ b/frontend/pages/schedule/ManageSchedulePage/ManageSchedulePage.tsx @@ -496,12 +496,12 @@ const ManageSchedulePage = ({ {selectedTeamId ? (

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

) : (

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

)}
diff --git a/frontend/pages/schedule/ManageSchedulePage/_styles.scss b/frontend/pages/schedule/ManageSchedulePage/_styles.scss index 63f6cce90f..962935e6b9 100644 --- a/frontend/pages/schedule/ManageSchedulePage/_styles.scss +++ b/frontend/pages/schedule/ManageSchedulePage/_styles.scss @@ -115,111 +115,6 @@ } } } - - &__empty-table { - text-align: center; - font-size: $x-small; - color: $core-fleet-black; - } - } - - .no-schedule { - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - padding-top: $pad-xxxlarge; - - h1 { - font-size: $large; - font-weight: $regular; - line-height: normal; - letter-spacing: normal; - color: $core-fleet-black; - } - - h2 { - font-size: $small; - font-weight: $bold; - margin: 0 0 $pad-large; - line-height: 20px; - color: $core-fleet-black; - } - - ul { - margin: 0; - padding: 0; - color: $core-fleet-black; - list-style: none; - - li { - &::before { - content: "•"; - color: $core-vibrant-blue; - margin-right: $pad-medium; - } - } - } - - &__inner { - display: flex; - flex-direction: column; - align-items: center; - - h1 { - font-size: $small; - font-weight: $bold; - margin-bottom: $pad-medium; - } - - img { - width: 322px; - } - - p { - color: $core-fleet-black; - font-weight: $regular; - font-size: $x-small; - margin: 0; - margin-bottom: $pad-large; - } - - .no-filter-results { - display: flex; - flex-direction: column; - width: 350px; - } - } - - &__inner-text { - width: 400px; - padding: $pad-xxlarge 0; - } - - &__schedule-button { - margin-right: $pad-medium; - } - - .query-pagination__pager-wrap { - margin-top: $pad-medium; - } - - &__no-hosts-contact { - text-align: left; - margin-top: $pad-large; - - p { - color: $core-fleet-black; - font-weight: $bold; - font-size: $x-small; - margin: 0; - } - } - - &__cta-buttons { - display: flex; - justify-content: center; - } } .no-team-schedule { diff --git a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx index c2851bdd81..80df601151 100644 --- a/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx +++ b/frontend/pages/schedule/ManageSchedulePage/components/ScheduleTable/ScheduleTable.tsx @@ -5,24 +5,24 @@ import React from "react"; import { InjectedRouter } from "react-router"; import paths from "router/paths"; -import Button from "components/buttons/Button"; import { IScheduledQuery, IEditScheduledQuery, } from "interfaces/scheduled_query"; - import { ITeam } from "interfaces/team"; +import { IEmptyTableProps } from "interfaces/empty_table"; +import Button from "components/buttons/Button"; +import CustomLink from "components/CustomLink"; import TableContainer from "components/TableContainer"; +import EmptyTable from "components/EmptyTable"; import { generateInheritedQueriesTableHeaders, generateTableHeaders, generateDataSet, } from "./ScheduleTableConfig"; -import scheduleSvg from "../../../../../../assets/images/no-schedule-322x138@2x.png"; const baseClass = "schedule-table"; -const noScheduleClass = "no-schedule"; const TAGGED_TEMPLATES = { hostsByTeamRoute: (teamId: number | undefined | null) => { @@ -60,70 +60,67 @@ const ScheduleTable = ({ const handleAdvanced = () => router.push(MANAGE_PACKS); - const NoScheduledQueries = () => { - return ( -
-
- No Schedule -
-

- - {selectedTeamData ? ( - <> - Schedule queries for all hosts assigned to{" "} - - {selectedTeamData.name} - - - ) : ( - <> - Schedule queries to run at regular intervals on{" "} - all your hosts - - )} - - {isOnGlobalTeam ? ( - <> - , -
or go to your osquery packs via the ‘Advanced’ button.{" "} - - ) : ( - <> - . - - )} -

-
- - {isOnGlobalTeam && ( - - )} -
-
-
-
- ); + const emptyState = () => { + const emptySchedule: IEmptyTableProps = { + iconName: "empty-schedule", + header: ( + <> + Schedule queries to run at regular intervals on{" "} + all your hosts + + ), + additionalInfo: ( + <> + Want to learn more?  + + + ), + primaryButton: ( + + ), + }; + + if (selectedTeamData) { + emptySchedule.header = ( + <> + Schedule queries for all hosts assigned to{" "} + + {selectedTeamData.name} + + + ); + } + + if (isOnGlobalTeam) { + emptySchedule.info = ( + <>Or go to your osquery packs via the ‘Advanced’ button. + ); + emptySchedule.secondaryButton = ( + + ); + } + return emptySchedule; }; const onActionSelection = ( @@ -171,7 +168,16 @@ const ScheduleTable = ({ searchable={false} disablePagination disableCount - emptyComponent={NoScheduledQueries} + emptyComponent={() => + EmptyTable({ + iconName: emptyState().iconName, + header: emptyState().header, + info: emptyState().info, + additionalInfo: emptyState().additionalInfo, + primaryButton: emptyState().primaryButton, + secondaryButton: emptyState().secondaryButton, + }) + } /> ); @@ -194,7 +200,16 @@ const ScheduleTable = ({ primarySelectActionButtonVariant="text-icon" primarySelectActionButtonIcon="remove" primarySelectActionButtonText={"Remove"} - emptyComponent={NoScheduledQueries} + emptyComponent={() => + EmptyTable({ + iconName: emptyState().iconName, + header: emptyState().header, + info: emptyState().info, + additionalInfo: emptyState().additionalInfo, + primaryButton: emptyState().primaryButton, + secondaryButton: emptyState().secondaryButton, + }) + } isClientSidePagination /> diff --git a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx index c0a0016428..d3d72cfa33 100644 --- a/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx +++ b/frontend/pages/software/ManageSoftwarePage/ManageSoftwarePage.tsx @@ -17,6 +17,7 @@ import { IConfig, CONFIG_DEFAULT_RECENT_VULNERABILITY_MAX_AGE_IN_DAYS, } from "interfaces/config"; +import { IEmptyTableProps } from "interfaces/empty_table"; import { IJiraIntegration, IZendeskIntegration, @@ -47,10 +48,10 @@ import TeamsDropdownHeader, { import LastUpdatedText from "components/LastUpdatedText"; import MainContent from "components/MainContent"; import CustomLink from "components/CustomLink"; +import EmptyTable from "components/EmptyTable"; import generateSoftwareTableHeaders from "./SoftwareTableConfig"; import ManageAutomationsModal from "./components/ManageAutomationsModal"; -import EmptySoftware from "../components/EmptySoftware"; interface IManageSoftwarePageProps { router: InjectedRouter; @@ -530,6 +531,43 @@ const ManageSoftwarePage = ({ router.push(path); }; + const emptyState = () => { + const emptySoftware: IEmptyTableProps = { + header: "No software match the current search criteria", + info: "Try again in about 1 hour as the system catches up.", + }; + if (!isSoftwareEnabled) { + emptySoftware.iconName = "empty-software"; + emptySoftware.header = "Software inventory disabled"; + emptySoftware.info = ( + <> + Users with the admin role can{" "} + + . + + ); + } + if (isCollectingInventory) { + emptySoftware.iconName = "empty-software"; + emptySoftware.header = "No software detected"; + emptySoftware.info = + "This report is updated every hour to protect the performance of your devices."; + } + if (currentTeam && filterVuln) { + emptySoftware.iconName = "empty-software"; + emptySoftware.header = "No vulnerable software detected"; + emptySoftware.info = + "This report is updated every hour to protect the performance of your devices."; + } + return emptySoftware; + }; + + const searchable = !!software?.software || searchQuery !== ""; + return !availableTeams || !globalConfig || (!softwareConfig && !softwareConfigError) ? ( @@ -549,11 +587,11 @@ const ManageSoftwarePage = ({ isLoading={isFetchingSoftware || isFetchingCount} resultsTitle={"software items"} emptyComponent={() => - EmptySoftware( - (!isSoftwareEnabled && "disabled") || - (isCollectingInventory && "collecting") || - "default" - ) + EmptyTable({ + iconName: emptyState().iconName, + header: emptyState().header, + info: emptyState().info, + }) } defaultSortHeader={DEFAULT_SORT_HEADER} defaultSortDirection={DEFAULT_SORT_DIRECTION} @@ -562,13 +600,13 @@ const ManageSoftwarePage = ({ showMarkAllPages={false} isAllPagesSelected={false} disableNextPage={isLastPage} - searchable + searchable={searchable} inputPlaceHolder="Search software by name or vulnerabilities (CVEs)" onQueryChange={onQueryChange} additionalQueries={filterVuln ? "vulnerable" : ""} // additionalQueries serves as a trigger // for the useDeepEffect hook to fire onQueryChange for events happeing outside of // the TableContainer - customControl={renderVulnFilterDropdown} + customControl={searchable ? renderVulnFilterDropdown : undefined} stackControls renderCount={renderSoftwareCount} renderFooter={renderTableFooter} diff --git a/frontend/pages/software/ManageSoftwarePage/_styles.scss b/frontend/pages/software/ManageSoftwarePage/_styles.scss index 5f5865f452..885b750d8b 100644 --- a/frontend/pages/software/ManageSoftwarePage/_styles.scss +++ b/frontend/pages/software/ManageSoftwarePage/_styles.scss @@ -56,31 +56,6 @@ } } - &__empty-software { - margin: 80px auto 0; - display: flex; - flex-direction: column; - align-items: center; - - .empty-software__inner { - display: flex; - flex-direction: column; - - h1 { - font-size: $small; - font-weight: $bold; - margin-bottom: $pad-medium; - } - - p { - color: $core-fleet-black; - font-weight: $regular; - font-size: $x-small; - margin: 0; - } - } - } - &__table { .table-container { &__header-left { diff --git a/frontend/pages/software/components/EmptySoftware.tsx b/frontend/pages/software/components/EmptySoftware.tsx deleted file mode 100644 index e4977d6156..0000000000 --- a/frontend/pages/software/components/EmptySoftware.tsx +++ /dev/null @@ -1,53 +0,0 @@ -// This component is used on ManageSoftwarePage.tsx and DashboardPage.tsx > Software.tsx card - -import React from "react"; - -import CustomLink from "components/CustomLink"; - -const baseClass = "manage-software-page"; - -type IEmptySoftware = "disabled" | "collecting" | "default" | ""; - -const EmptySoftware = (message: IEmptySoftware): JSX.Element => { - switch (message) { - case "disabled": { - return ( -
-
-

Software inventory is disabled.

-

- Check out the Fleet documentation on{" "} - -

-
-
- ); - } - case "collecting": { - return ( -
-
-

Fleet is collecting software inventory.

-

Try again in about 1 hour as the system catches up.

-
-
- ); - } - default: { - return ( -
-
-

No software matches the current search criteria.

-

Try again in about 1 hour as the system catches up.

-
-
- ); - } - } -}; - -export default EmptySoftware; From f2bdf7139e631e33b963110b01017f8d8237c5b3 Mon Sep 17 00:00:00 2001 From: Jacob Shandling <61553566+jacobshandling@users.noreply.github.com> Date: Thu, 5 Jan 2023 07:08:27 -0800 Subject: [PATCH 4/6] UI hackathon - add datetime details in tooltip for most "Last Xed" data presentations (#9166) --- .../UI-hackathon-localized-datetime-tooltips | 1 + .../HumanTimeDiffWithDateTip.tsx | 40 +++++++++++++++++++ .../HumanTimeDiffWithDateTip/index.ts | 1 + .../DataTable/TextCell/TextCell.tsx | 5 +-- .../ActivityItem/ActivityItem.tsx | 31 ++++++++++++-- .../UserSidePanel/UserSidePanel.tsx | 10 ++--- .../hosts/ManageHostsPage/HostTableConfig.tsx | 18 ++++++--- .../pages/hosts/details/cards/About/About.tsx | 19 +++++---- .../details/cards/HostSummary/HostSummary.tsx | 18 +++++---- frontend/utilities/helpers.ts | 19 ++++++++- 10 files changed, 127 insertions(+), 35 deletions(-) create mode 100644 changes/UI-hackathon-localized-datetime-tooltips create mode 100644 frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tsx create mode 100644 frontend/components/HumanTimeDiffWithDateTip/index.ts diff --git a/changes/UI-hackathon-localized-datetime-tooltips b/changes/UI-hackathon-localized-datetime-tooltips new file mode 100644 index 0000000000..dee8f16cbd --- /dev/null +++ b/changes/UI-hackathon-localized-datetime-tooltips @@ -0,0 +1 @@ +* Add locally-formated datetime tooltips for all "last Xed" data diff --git a/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tsx b/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tsx new file mode 100644 index 0000000000..eb0259fd4f --- /dev/null +++ b/frontend/components/HumanTimeDiffWithDateTip/HumanTimeDiffWithDateTip.tsx @@ -0,0 +1,40 @@ +import React from "react"; + +import { uniqueId } from "lodash"; +import { humanHostLastSeen } from "utilities/helpers"; +import ReactTooltip from "react-tooltip"; +import intlFormat from "date-fns/intlFormat"; + +export default ({ timeString }: { timeString: string }): JSX.Element => { + const id = uniqueId(); + return timeString === "Unavailable" ? ( + Unavailable + ) : ( + <> + + {humanHostLastSeen(timeString)} + + + {intlFormat( + new Date(timeString), + { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }, + { locale: window.navigator.languages[0] } + )} + + + ); +}; diff --git a/frontend/components/HumanTimeDiffWithDateTip/index.ts b/frontend/components/HumanTimeDiffWithDateTip/index.ts new file mode 100644 index 0000000000..d2b716c68d --- /dev/null +++ b/frontend/components/HumanTimeDiffWithDateTip/index.ts @@ -0,0 +1 @@ +export { default } from "./HumanTimeDiffWithDateTip"; diff --git a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx index 90a5e9cc36..d36f33f6c2 100644 --- a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx +++ b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx @@ -1,8 +1,8 @@ import React from "react"; interface ITextCellProps { - value: string | number | boolean; - formatter?: (val: any) => string; // string, number, or null + value: string | number | boolean | { timeString: string }; + formatter?: (val: any) => JSX.Element | string; // string, number, or null greyed?: boolean; classes?: string; } @@ -18,7 +18,6 @@ const TextCell = ({ if (typeof value === "boolean") { val = value.toString(); } - return ( {formatter(val)} diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx index 8f51040deb..d249cb8121 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/ActivityItem.tsx @@ -1,12 +1,13 @@ import React from "react"; import { find, lowerCase, noop } from "lodash"; -import { formatDistanceToNowStrict } from "date-fns"; +import { intlFormat, formatDistanceToNowStrict } from "date-fns"; import { ActivityType, IActivity, IActivityDetails } from "interfaces/activity"; import { addGravatarUrlToResource } from "utilities/helpers"; import Avatar from "components/Avatar"; import Button from "components/buttons/Button"; import Icon from "components/Icon"; +import ReactTooltip from "react-tooltip"; const baseClass = "activity-item"; @@ -237,6 +238,8 @@ const ActivityItem = ({ ? addGravatarUrlToResource({ email: actor_email }) : { gravatarURL: DEFAULT_GRAVATAR_URL }; + const activityCreatedAt = new Date(activity.created_at); + return (
- {formatDistanceToNowStrict(new Date(activity.created_at), { + {formatDistanceToNowStrict(activityCreatedAt, { addSuffix: true, })} + + {intlFormat( + activityCreatedAt, + { + year: "numeric", + month: "numeric", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + }, + { locale: window.navigator.languages[0] } + )} +

diff --git a/frontend/pages/UserSettingsPage/UserSidePanel/UserSidePanel.tsx b/frontend/pages/UserSettingsPage/UserSidePanel/UserSidePanel.tsx index c49fae88e6..a9215b8940 100644 --- a/frontend/pages/UserSettingsPage/UserSidePanel/UserSidePanel.tsx +++ b/frontend/pages/UserSettingsPage/UserSidePanel/UserSidePanel.tsx @@ -1,5 +1,4 @@ import React, { useContext, useEffect, useState } from "react"; -import { formatDistanceToNow } from "date-fns"; import { IUser } from "interfaces/user"; import { IVersionData } from "interfaces/version"; @@ -10,6 +9,7 @@ import versionAPI from "services/entities/version"; import Avatar from "components/Avatar"; import Button from "components/buttons/Button"; +import HumanTimeDiffWithDateTip from "components/HumanTimeDiffWithDateTip"; import { generateRole, generateTeam, greyCell } from "utilities/helpers"; @@ -53,11 +53,9 @@ const UserSidePanel = ({ const roleText = generateRole(teams, globalRole); const teamsText = generateTeam(teams, globalRole); - const lastUpdatedAt = - updatedAt && - formatDistanceToNow(new Date(updatedAt), { - addSuffix: true, - }); + const lastUpdatedAt = updatedAt && ( + + ); return (
diff --git a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx index 5f41c73180..5dd007e54f 100644 --- a/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx +++ b/frontend/pages/hosts/ManageHostsPage/HostTableConfig.tsx @@ -14,11 +14,11 @@ import LinkCell from "components/TableContainer/DataTable/LinkCell/LinkCell"; import StatusIndicator from "components/StatusIndicator"; import TextCell from "components/TableContainer/DataTable/TextCell/TextCell"; import TooltipWrapper from "components/TooltipWrapper"; +import HumanTimeDiffWithDateTip from "components/HumanTimeDiffWithDateTip"; import { humanHostMemory, humanHostLastRestart, humanHostLastSeen, - humanHostDetailUpdated, hostTeamName, } from "utilities/helpers"; import { IConfig } from "interfaces/config"; @@ -361,8 +361,8 @@ const allHostTableHeaders: IDataColumn[] = [ accessor: "detail_updated_at", Cell: (cellProps: ICellProps) => ( ), }, @@ -387,7 +387,10 @@ const allHostTableHeaders: IDataColumn[] = [ }, accessor: "seen_time", Cell: (cellProps: ICellProps) => ( - + ), }, { @@ -414,7 +417,12 @@ const allHostTableHeaders: IDataColumn[] = [ const { uptime, detail_updated_at } = cellProps.row.original; return ( - + ); }, }, diff --git a/frontend/pages/hosts/details/cards/About/About.tsx b/frontend/pages/hosts/details/cards/About/About.tsx index 1ba5906b36..70a8760bfc 100644 --- a/frontend/pages/hosts/details/cards/About/About.tsx +++ b/frontend/pages/hosts/details/cards/About/About.tsx @@ -1,9 +1,10 @@ import React from "react"; import ReactTooltip from "react-tooltip"; +import HumanTimeDiffWithDateTip from "components/HumanTimeDiffWithDateTip"; import { IHostMdmData, IMunkiData, IDeviceUser } from "interfaces/host"; -import { humanHostLastRestart, humanHostEnrolled } from "utilities/helpers"; +import { humanHostLastRestart } from "utilities/helpers"; interface IAboutProps { aboutData: { [key: string]: any }; @@ -18,7 +19,6 @@ const About = ({ deviceMapping, munki, mdm, - wrapFleetHelper, }: IAboutProps): JSX.Element => { const renderSerialAndIPs = () => { return ( @@ -143,7 +143,6 @@ const About = ({
); }; - return (

About

@@ -151,16 +150,20 @@ const About = ({
Added to Fleet - {wrapFleetHelper(humanHostEnrolled, aboutData.last_enrolled_at)} +
Last restarted - {humanHostLastRestart( - aboutData.detail_updated_at, - aboutData.uptime - )} +
diff --git a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx index 3e93a8ca5e..dd07c0623a 100644 --- a/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx +++ b/frontend/pages/hosts/details/cards/HostSummary/HostSummary.tsx @@ -5,11 +5,8 @@ import TooltipWrapper from "components/TooltipWrapper"; import Button from "components/buttons/Button"; import DiskSpaceGraph from "components/DiskSpaceGraph"; -import { - humanHostMemory, - humanHostDetailUpdated, - wrapFleetHelper, -} from "utilities/helpers"; +import HumanTimeDiffWithDateTip from "components/HumanTimeDiffWithDateTip"; +import { humanHostMemory, wrapFleetHelper } from "utilities/helpers"; import getHostStatusTooltipText from "pages/hosts/helpers"; import StatusIndicator from "components/StatusIndicator"; import IssueIcon from "../../../../../../assets/images/icon-issue-fleet-black-50-16x16@2x.png"; @@ -203,6 +200,12 @@ const HostSummary = ({ ); }; + const lastFetched = titleData.detail_updated_at ? ( + + ) : ( + ": unavailable" + ); + return ( <>
@@ -211,10 +214,9 @@ const HostSummary = ({

{deviceUser ? "My device" : titleData.display_name || "---"}

+

- {`Last fetched ${humanHostDetailUpdated( - titleData.detail_updated_at - )}`} + {"Last fetched"} {lastFetched}  

{renderRefetch()} diff --git a/frontend/utilities/helpers.ts b/frontend/utilities/helpers.ts index 9087231f2c..22370f0ed9 100644 --- a/frontend/utilities/helpers.ts +++ b/frontend/utilities/helpers.ts @@ -1,10 +1,22 @@ -import { isEmpty, flatMap, omit, pick, size, memoize, reduce } from "lodash"; +import React from "react"; +import ReactTooltip from "react-tooltip"; +import { + isEmpty, + flatMap, + omit, + pick, + size, + memoize, + reduce, + uniqueId, +} from "lodash"; import md5 from "js-md5"; import { formatDistanceToNow, isAfter, intervalToDuration, formatDuration, + intlFormat, } from "date-fns"; import yaml from "js-yaml"; @@ -579,7 +591,7 @@ export const humanHostLastRestart = ( restartDate.getMilliseconds() - millisecondsLastRestart ); - return formatDistanceToNow(new Date(restartDate), { addSuffix: true }); + return restartDate.toString(); } catch { return "Unavailable"; } @@ -589,6 +601,9 @@ export const humanHostLastSeen = (lastSeen: string): string => { if (!lastSeen || lastSeen < "2016-07-28T00:00:00Z") { return "Never"; } + if (lastSeen === "Unavailable") { + return "Unavailable"; + } return formatDistanceToNow(new Date(lastSeen), { addSuffix: true }); }; From a2d672435deda29cd2ff483b43b4f24bda1d44fd Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Thu, 5 Jan 2023 15:23:27 +0000 Subject: [PATCH 5/6] update buttons to match new styleguide (#9183) * update button to follow new style guide * update button styles for inverted ghost buttons * update a color name to match new styleguide --- changes/ui-hackathon-change-buttons | 1 + .../components/DiskSpaceGraph/_styles.scss | 2 +- .../EnrollSecretModal/_styles.scss | 2 +- .../SecretEditorModal/_styles.scss | 2 +- frontend/components/FlashMessage/_styles.scss | 2 +- frontend/components/Modal/_styles.scss | 2 +- .../DefaultColumnFilter/_styles.scss | 2 +- .../TableContainer/DataTable/_styles.scss | 10 ++-- .../components/buttons/Button/_styles.scss | 60 ++++++++++++------- .../forms/RegistrationForm/_styles.scss | 2 +- .../forms/fields/Dropdown/_styles.scss | 2 +- .../forms/fields/InputField/_styles.scss | 2 +- .../fields/InputFieldWithIcon/_styles.scss | 2 +- .../forms/fields/Radio/_styles.scss | 2 +- .../SelectTargetsMenu/_styles.scss | 4 +- .../fields/SelectTargetsDropdown/_styles.scss | 2 +- .../queries/PackQueriesTable/_styles.scss | 2 +- .../PackInfoSidePanel/_styles.scss | 2 +- .../EventedTableTag/_styles.scss | 2 +- .../side_panels/QuerySidePanel/_styles.scss | 2 +- .../components/top_nav/UserMenu/_styles.scss | 2 +- .../ActivityFeed/ActivityItem/_styles.scss | 2 +- .../components/InfoCard/_styles.scss | 4 +- .../components/OrgSettingsForm/_styles.scss | 4 +- .../IntegrationsPage/cards/Mdm/_styles.scss | 2 +- .../MembersPage/_styles.scss | 2 +- .../AutocompleteDropdown/_styles.scss | 2 +- .../components/SelectedTeamsForm/_styles.scss | 2 +- .../CustomLabelGroupHeading/_styles.scss | 2 +- .../components/LabelFilterSelect/_styles.scss | 2 +- .../hosts/details/DeviceUserPage/_styles.scss | 4 +- .../details/HostDetailsPage/_styles.scss | 2 +- .../ManageAutomationsModal/_styles.scss | 2 +- .../PolicyQueriesErrorsTable/_styles.scss | 4 +- .../PolicyQueriesTable/_styles.scss | 4 +- .../ManageAutomationsModal/_styles.scss | 2 +- .../software/SoftwareDetailsPage/_styles.scss | 2 +- frontend/styles/global/_global.scss | 2 +- frontend/styles/var/colors.scss | 7 ++- frontend/styles/var/colors.ts | 2 +- 40 files changed, 91 insertions(+), 71 deletions(-) create mode 100644 changes/ui-hackathon-change-buttons diff --git a/changes/ui-hackathon-change-buttons b/changes/ui-hackathon-change-buttons new file mode 100644 index 0000000000..e227908fb4 --- /dev/null +++ b/changes/ui-hackathon-change-buttons @@ -0,0 +1 @@ +- update buttons to the the new style guide. diff --git a/frontend/components/DiskSpaceGraph/_styles.scss b/frontend/components/DiskSpaceGraph/_styles.scss index c07aeba038..734a76dcc4 100644 --- a/frontend/components/DiskSpaceGraph/_styles.scss +++ b/frontend/components/DiskSpaceGraph/_styles.scss @@ -7,7 +7,7 @@ display: inline-block; height: 4px; width: 50px; - background-color: $ui-fleet-blue-15; + background-color: $ui-fleet-black-10; border-radius: 2px; margin-right: $pad-small; overflow: hidden; diff --git a/frontend/components/EnrollSecrets/EnrollSecretModal/_styles.scss b/frontend/components/EnrollSecrets/EnrollSecretModal/_styles.scss index d94276ffaa..5eb84b181c 100644 --- a/frontend/components/EnrollSecrets/EnrollSecretModal/_styles.scss +++ b/frontend/components/EnrollSecrets/EnrollSecretModal/_styles.scss @@ -13,7 +13,7 @@ code { background-color: $ui-off-white; color: $core-fleet-blue; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; border-radius: 4px; padding: 7px $pad-medium; margin: $pad-large 0 0 44px; diff --git a/frontend/components/EnrollSecrets/SecretEditorModal/_styles.scss b/frontend/components/EnrollSecrets/SecretEditorModal/_styles.scss index 7e2a04ea17..1dd288204a 100644 --- a/frontend/components/EnrollSecrets/SecretEditorModal/_styles.scss +++ b/frontend/components/EnrollSecrets/SecretEditorModal/_styles.scss @@ -13,7 +13,7 @@ code { background-color: $ui-off-white; color: $core-fleet-blue; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; border-radius: 4px; padding: 7px $pad-medium; margin: $pad-large 0 0 44px; diff --git a/frontend/components/FlashMessage/_styles.scss b/frontend/components/FlashMessage/_styles.scss index d7b96aa124..75163039a8 100644 --- a/frontend/components/FlashMessage/_styles.scss +++ b/frontend/components/FlashMessage/_styles.scss @@ -21,7 +21,7 @@ z-index: 999; background-color: $core-vibrant-blue; margin: auto; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; box-sizing: border-box; box-shadow: 0px 7px 3px -5px rgba(25, 33, 71, 0.1); border-radius: 8px; diff --git a/frontend/components/Modal/_styles.scss b/frontend/components/Modal/_styles.scss index 77fb627ee9..101eadd268 100644 --- a/frontend/components/Modal/_styles.scss +++ b/frontend/components/Modal/_styles.scss @@ -53,7 +53,7 @@ font-weight: $regular; text-align: left; padding-bottom: $pad-xsmall; - border-bottom: 1px solid $ui-fleet-blue-15; + border-bottom: 1px solid $ui-fleet-black-10; display: flex; justify-content: space-between; diff --git a/frontend/components/TableContainer/DataTable/DefaultColumnFilter/_styles.scss b/frontend/components/TableContainer/DataTable/DefaultColumnFilter/_styles.scss index aef79668c5..7a72223a00 100644 --- a/frontend/components/TableContainer/DataTable/DefaultColumnFilter/_styles.scss +++ b/frontend/components/TableContainer/DataTable/DefaultColumnFilter/_styles.scss @@ -4,7 +4,7 @@ width: 100%; font-size: $x-small; background-color: $ui-off-white; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; border-radius: 4px; padding: 4px; padding-left: 20px; diff --git a/frontend/components/TableContainer/DataTable/_styles.scss b/frontend/components/TableContainer/DataTable/_styles.scss index 3d2da37f70..301031827a 100644 --- a/frontend/components/TableContainer/DataTable/_styles.scss +++ b/frontend/components/TableContainer/DataTable/_styles.scss @@ -9,14 +9,14 @@ $shadow-transition-width: 10px; .data-table { &__wrapper { position: relative; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; border-radius: 6px; margin-top: $pad-small; flex-grow: 1; width: 100%; // Shadow - background-image: + background-image: /* Shadows */ linear-gradient( to right, white, @@ -73,7 +73,7 @@ $shadow-transition-width: 10px; } tr { - border-bottom: 1px solid $ui-fleet-blue-15; + border-bottom: 1px solid $ui-fleet-black-10; &:last-child { border-bottom: 0; @@ -95,7 +95,7 @@ $shadow-transition-width: 10px; background-color: $ui-off-white-opaque; // opaque needed for horizontal scroll shadow color: $core-fleet-black; text-align: left; - border-bottom: 1px solid $ui-fleet-blue-15; + border-bottom: 1px solid $ui-fleet-black-10; // resize header icons img { @@ -116,7 +116,7 @@ $shadow-transition-width: 10px; th { padding: $pad-medium $pad-large; white-space: nowrap; - border-right: 1px solid $ui-fleet-blue-15; + border-right: 1px solid $ui-fleet-black-10; &:first-child { border-top-left-radius: 6px; diff --git a/frontend/components/buttons/Button/_styles.scss b/frontend/components/buttons/Button/_styles.scss index 099620b4df..d96adcfba1 100644 --- a/frontend/components/buttons/Button/_styles.scss +++ b/frontend/components/buttons/Button/_styles.scss @@ -1,27 +1,51 @@ $base-class: "button"; -@mixin button-variant($color, $hover: null, $active: null, $inverse: null) { +@mixin button-focus-outline($offset: 2px) { + outline-color: #d9d9fe; + outline-offset: $offset; + outline-style: solid; + outline-width: 2px; +} + +@mixin button-variant($color, $hover: null, $active: null, $inverse: false) { background-color: $color; @if $inverse { - &:hover, - &:focus { - border: 2px solid $hover; - color: $hover; + &:hover { + background-color: rgba(#192147, .05); + + &:active { + background-color: $ui-fleet-black-10; + } } - &:active { - border: 2px solid $active; - color: $active; + &:focus-visible { + // need a slightly larger focus outline to accomodate the :after content box + // that correctly displays the border. We chose this approach as adding a + // border to the button caused the button to jump around on the screen + // when it was added and removed. + @include button-focus-outline($offset: 3px); + &::after { + content: ""; + width: 100%; + height: 100%; + position: absolute; + border: 1px solid $core-vibrant-blue; + border-radius: 6px; + } } + } @else { - &:hover:not(.button--disabled), - &:focus { + &:hover:not(.button--disabled) { background-color: $hover; + + &:active { + background-color: $active; + } } - &:active { - background-color: $active; + &:focus-visible { + @include button-focus-outline() } } } @@ -37,7 +61,7 @@ $base-class: "button"; justify-content: center; align-items: center; padding: $pad-small $pad-medium; - border-radius: 4px; + border-radius: 6px; font-size: $x-small; font-family: "Nunito Sans", sans-serif; font-weight: $bold; @@ -241,14 +265,8 @@ $base-class: "button"; $inverse: true ); color: $core-vibrant-blue; - border: 0; box-sizing: border-box; - padding: 0; - - &:hover, - &:focus { - border: 0; - } + padding: $pad-small; } &--inverse-alert { @@ -316,7 +334,7 @@ $base-class: "button"; display: block; width: 100%; border-radius: 0px; - border-bottom: 1px solid $ui-fleet-blue-15; + border-bottom: 1px solid $ui-fleet-black-10; &:active { box-shadow: none; diff --git a/frontend/components/forms/RegistrationForm/_styles.scss b/frontend/components/forms/RegistrationForm/_styles.scss index 9da41c1d6e..91388d3574 100644 --- a/frontend/components/forms/RegistrationForm/_styles.scss +++ b/frontend/components/forms/RegistrationForm/_styles.scss @@ -23,7 +23,7 @@ padding: 0 0 $pad-medium; margin: 0; margin-bottom: $pad-xxlarge; - border-bottom: 1px solid $ui-fleet-blue-15; + border-bottom: 1px solid $ui-fleet-black-10; } p { diff --git a/frontend/components/forms/fields/Dropdown/_styles.scss b/frontend/components/forms/fields/Dropdown/_styles.scss index f25c4caa90..0ca93bfbab 100644 --- a/frontend/components/forms/fields/Dropdown/_styles.scss +++ b/frontend/components/forms/fields/Dropdown/_styles.scss @@ -44,7 +44,7 @@ .Select { &.dropdown__select { - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; border-radius: $border-radius; &:hover { box-shadow: none; diff --git a/frontend/components/forms/fields/InputField/_styles.scss b/frontend/components/forms/fields/InputField/_styles.scss index 697d9e2fcf..f5d6ef3cb7 100644 --- a/frontend/components/forms/fields/InputField/_styles.scss +++ b/frontend/components/forms/fields/InputField/_styles.scss @@ -1,7 +1,7 @@ .input-field { line-height: 1.5; background-color: $ui-light-grey; - border: solid 1px $ui-fleet-blue-15; + border: solid 1px $ui-fleet-black-10; border-radius: 4px; font-size: $small; padding: 7px 12px; diff --git a/frontend/components/forms/fields/InputFieldWithIcon/_styles.scss b/frontend/components/forms/fields/InputFieldWithIcon/_styles.scss index 6f5e299ec4..749087831c 100644 --- a/frontend/components/forms/fields/InputFieldWithIcon/_styles.scss +++ b/frontend/components/forms/fields/InputFieldWithIcon/_styles.scss @@ -22,7 +22,7 @@ } &__input { - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; background-color: $ui-light-grey; border-radius: $border-radius; padding: 9px 30px 9px $pad-medium; diff --git a/frontend/components/forms/fields/Radio/_styles.scss b/frontend/components/forms/fields/Radio/_styles.scss index a201a2f2e3..e254fb2642 100644 --- a/frontend/components/forms/fields/Radio/_styles.scss +++ b/frontend/components/forms/fields/Radio/_styles.scss @@ -45,7 +45,7 @@ width: 16px; height: 16px; border-radius: 50%; - border: 2px solid $ui-fleet-blue-15; + border: 2px solid $ui-fleet-black-10; transform: translateY(-0.05em); } diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/_styles.scss b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/_styles.scss index 1097f73182..036ac08f43 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/_styles.scss +++ b/frontend/components/forms/fields/SelectTargetsDropdown/SelectTargetsMenu/_styles.scss @@ -7,7 +7,7 @@ font-weight: $bold; font-size: $x-small; color: $core-fleet-black; - border-bottom: 1px solid $ui-fleet-blue-15; + border-bottom: 1px solid $ui-fleet-black-10; &::first-letter { text-transform: uppercase; @@ -24,7 +24,7 @@ overflow-y: scroll; max-height: 100%; padding: 0 $pad-large; - border-right: 1px solid $ui-fleet-blue-15; + border-right: 1px solid $ui-fleet-black-10; background-color: $core-white; } diff --git a/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss b/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss index 8c322b8d36..fd69e54861 100644 --- a/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss +++ b/frontend/components/forms/fields/SelectTargetsDropdown/_styles.scss @@ -28,7 +28,7 @@ &.Select { border-radius: $border-radius; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; &.is-focused, &:hover { diff --git a/frontend/components/queries/PackQueriesTable/_styles.scss b/frontend/components/queries/PackQueriesTable/_styles.scss index 8c79e0ec9b..7a3483d891 100644 --- a/frontend/components/queries/PackQueriesTable/_styles.scss +++ b/frontend/components/queries/PackQueriesTable/_styles.scss @@ -34,7 +34,7 @@ } @media (min-width: $break-990) { .interval__header { - border-right: 1px solid $ui-fleet-blue-15; + border-right: 1px solid $ui-fleet-black-10; } .performance__header { display: table-cell; diff --git a/frontend/components/side_panels/PackInfoSidePanel/_styles.scss b/frontend/components/side_panels/PackInfoSidePanel/_styles.scss index dfb91a2502..e2c2d016ca 100644 --- a/frontend/components/side_panels/PackInfoSidePanel/_styles.scss +++ b/frontend/components/side_panels/PackInfoSidePanel/_styles.scss @@ -3,7 +3,7 @@ font-size: $medium; font-weight: $regular; color: $core-fleet-black; - border-bottom: 1px solid $ui-fleet-blue-15; + border-bottom: 1px solid $ui-fleet-black-10; padding-bottom: 8px; margin: 0 0 7px; } diff --git a/frontend/components/side_panels/QuerySidePanel/EventedTableTag/_styles.scss b/frontend/components/side_panels/QuerySidePanel/EventedTableTag/_styles.scss index 8925d9270d..75455117fd 100644 --- a/frontend/components/side_panels/QuerySidePanel/EventedTableTag/_styles.scss +++ b/frontend/components/side_panels/QuerySidePanel/EventedTableTag/_styles.scss @@ -2,7 +2,7 @@ display: inline-flex; align-items: center; color: $core-fleet-black; - background-color: $ui-fleet-blue-15; + background-color: $ui-fleet-black-10; padding: $pad-xsmall $pad-small; border-radius: 6px; font-size: $xxx-small; diff --git a/frontend/components/side_panels/QuerySidePanel/_styles.scss b/frontend/components/side_panels/QuerySidePanel/_styles.scss index 6cf9484115..7dcebdff19 100644 --- a/frontend/components/side_panels/QuerySidePanel/_styles.scss +++ b/frontend/components/side_panels/QuerySidePanel/_styles.scss @@ -45,7 +45,7 @@ &__table-count { line-height: normal; margin-left: $pad-small; - background-color: $ui-fleet-blue-15; + background-color: $ui-fleet-black-10; padding: $pad-xsmall $pad-small; border-radius: 8px; font-size: $x-small; diff --git a/frontend/components/top_nav/UserMenu/_styles.scss b/frontend/components/top_nav/UserMenu/_styles.scss index 5c4b8745d4..cc55512774 100644 --- a/frontend/components/top_nav/UserMenu/_styles.scss +++ b/frontend/components/top_nav/UserMenu/_styles.scss @@ -27,7 +27,7 @@ .dropdown-button__option:last-child, .dropdown-button__option:nth-last-child(2) { - border-top: 1px solid $ui-fleet-blue-15; + border-top: 1px solid $ui-fleet-black-10; } } } diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/_styles.scss b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/_styles.scss index 1e17acf0c4..07257f00df 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/_styles.scss +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityItem/_styles.scss @@ -15,7 +15,7 @@ z-index: 0; top: 35px; left: 17px; - border-left: 1px dashed $ui-fleet-blue-15; + border-left: 1px dashed $ui-fleet-black-10; height: 100%; } diff --git a/frontend/pages/DashboardPage/components/InfoCard/_styles.scss b/frontend/pages/DashboardPage/components/InfoCard/_styles.scss index 9202a02a5a..777dcac9ab 100644 --- a/frontend/pages/DashboardPage/components/InfoCard/_styles.scss +++ b/frontend/pages/DashboardPage/components/InfoCard/_styles.scss @@ -2,9 +2,9 @@ padding: 32px; width: 100%; background-color: $core-white; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; border-radius: 8px; - box-shadow: 0 2px 0 0 $ui-fleet-blue-15; + box-shadow: 0 2px 0 0 $ui-fleet-black-10; box-sizing: border-box; &__section-title-cta { diff --git a/frontend/pages/admin/AppSettingsPage/components/OrgSettingsForm/_styles.scss b/frontend/pages/admin/AppSettingsPage/components/OrgSettingsForm/_styles.scss index 106c0cf905..e040e823a7 100644 --- a/frontend/pages/admin/AppSettingsPage/components/OrgSettingsForm/_styles.scss +++ b/frontend/pages/admin/AppSettingsPage/components/OrgSettingsForm/_styles.scss @@ -99,7 +99,7 @@ font-size: $medium; font-weight: $regular; color: $core-fleet-black; - border-bottom: solid 1px $ui-fleet-blue-15; + border-bottom: solid 1px $ui-fleet-black-10; margin: 0 0 $pad-xxlarge; @media (min-width: $break-990) { max-width: 65%; @@ -219,7 +219,7 @@ border-radius: 20%; height: 120px; width: 120px; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; background-color: $ui-light-grey; position: relative; bottom: -20px; diff --git a/frontend/pages/admin/IntegrationsPage/cards/Mdm/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/Mdm/_styles.scss index ffaba6093c..ca2e86b6d9 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/Mdm/_styles.scss +++ b/frontend/pages/admin/IntegrationsPage/cards/Mdm/_styles.scss @@ -13,7 +13,7 @@ font-size: $medium; font-weight: $regular; color: $core-fleet-black; - border-bottom: solid 1px $ui-fleet-blue-15; + border-bottom: solid 1px $ui-fleet-black-10; margin: 0 0 $pad-xxlarge; @media (min-width: $break-990) { max-width: 65%; diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/_styles.scss b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/_styles.scss index 806be61f4e..96e77ae4f7 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/_styles.scss +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/_styles.scss @@ -24,7 +24,7 @@ @media (min-width: $break-1400) { .role__header { - border-right: 1px solid $ui-fleet-blue-15; + border-right: 1px solid $ui-fleet-black-10; } } } diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/components/AutocompleteDropdown/_styles.scss b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/components/AutocompleteDropdown/_styles.scss index 0592741577..762a32ab2e 100644 --- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/components/AutocompleteDropdown/_styles.scss +++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/MembersPage/components/AutocompleteDropdown/_styles.scss @@ -1,6 +1,6 @@ .autocomplete-dropdown { .Select { - border: solid 1px $ui-fleet-blue-15; + border: solid 1px $ui-fleet-black-10; border-radius: 4px; &:hover, diff --git a/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/_styles.scss b/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/_styles.scss index 9c99fe8ecf..0ce4d44c04 100644 --- a/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/_styles.scss +++ b/frontend/pages/admin/UserManagementPage/components/SelectedTeamsForm/_styles.scss @@ -1,5 +1,5 @@ .selected-teams-form { - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; border-radius: $border-radius; background-color: $ui-off-white; padding: $pad-medium; diff --git a/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/_styles.scss index f39bfe91b3..0fa641a929 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/components/CustomLabelGroupHeading/_styles.scss @@ -37,7 +37,7 @@ width: 100%; line-height: 1.5; background-color: $ui-light-grey; - border: solid 1px $ui-fleet-blue-15; + border: solid 1px $ui-fleet-black-10; border-radius: 4px; font-size: $small; padding: $pad-xsmall 12px $pad-xsmall 42px; diff --git a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/_styles.scss index fbb8a4bdda..16d3de610f 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/_styles.scss +++ b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/_styles.scss @@ -11,7 +11,7 @@ } .label-filter-select__control { - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; background-color: $ui-light-grey; border-radius: $border-radius; height: 40px; diff --git a/frontend/pages/hosts/details/DeviceUserPage/_styles.scss b/frontend/pages/hosts/details/DeviceUserPage/_styles.scss index db025dc4e9..9a6c524f49 100644 --- a/frontend/pages/hosts/details/DeviceUserPage/_styles.scss +++ b/frontend/pages/hosts/details/DeviceUserPage/_styles.scss @@ -30,7 +30,7 @@ flex-direction: column; background-color: $core-white; border-radius: 16px; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; padding: $pad-xxlarge; box-shadow: 0px 3px 0px rgba(226, 228, 234, 0.4); @@ -335,7 +335,7 @@ } &__wrapper { - border: solid 1px $ui-fleet-blue-15; + border: solid 1px $ui-fleet-black-10; border-radius: 6px; margin-top: $pad-small; overflow: scroll; diff --git a/frontend/pages/hosts/details/HostDetailsPage/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/_styles.scss index d66102c79f..4e1831ecf9 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/_styles.scss +++ b/frontend/pages/hosts/details/HostDetailsPage/_styles.scss @@ -18,7 +18,7 @@ flex-direction: column; background-color: $core-white; border-radius: 16px; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; padding: $pad-xxlarge; box-shadow: 0px 3px 0px rgba(226, 228, 234, 0.4); diff --git a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss index b045cc780e..a557903296 100644 --- a/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss +++ b/frontend/pages/policies/ManagePoliciesPage/components/ManageAutomationsModal/_styles.scss @@ -5,7 +5,7 @@ code { background-color: $ui-off-white; color: $core-fleet-blue; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; border-radius: 4px; padding: 7px $pad-medium; margin: $pad-large 0 0 44px; diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsTable/_styles.scss b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsTable/_styles.scss index 1703f16186..4e110ad1dd 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsTable/_styles.scss +++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesErrorsTable/_styles.scss @@ -2,7 +2,7 @@ border-collapse: collapse; &__wrapper { - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; border-radius: 4px; overflow: hidden; margin-top: $pad-medium; @@ -14,7 +14,7 @@ thead { background-color: $ui-off-white; - border-bottom: 1px solid $ui-fleet-blue-15; + border-bottom: 1px solid $ui-fleet-black-10; th { font-size: $x-small; diff --git a/frontend/pages/policies/PolicyPage/components/PolicyQueriesTable/_styles.scss b/frontend/pages/policies/PolicyPage/components/PolicyQueriesTable/_styles.scss index 55f3864ac7..d0d5d05d28 100644 --- a/frontend/pages/policies/PolicyPage/components/PolicyQueriesTable/_styles.scss +++ b/frontend/pages/policies/PolicyPage/components/PolicyQueriesTable/_styles.scss @@ -2,7 +2,7 @@ border-collapse: collapse; &__wrapper { - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; border-radius: 4px; overflow: hidden; margin-top: $pad-medium; @@ -18,7 +18,7 @@ thead { background-color: $ui-off-white; - border-bottom: 1px solid $ui-fleet-blue-15; + border-bottom: 1px solid $ui-fleet-black-10; th { font-size: $x-small; diff --git a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss index e3ce395174..b9a1212b06 100644 --- a/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss +++ b/frontend/pages/software/ManageSoftwarePage/components/ManageAutomationsModal/_styles.scss @@ -5,7 +5,7 @@ code { background-color: $ui-off-white; color: $core-fleet-blue; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; border-radius: 4px; padding: 7px $pad-medium; margin: $pad-large 0 0 44px; diff --git a/frontend/pages/software/SoftwareDetailsPage/_styles.scss b/frontend/pages/software/SoftwareDetailsPage/_styles.scss index 4bc4d11fbb..3d8a569222 100644 --- a/frontend/pages/software/SoftwareDetailsPage/_styles.scss +++ b/frontend/pages/software/SoftwareDetailsPage/_styles.scss @@ -18,7 +18,7 @@ flex-direction: column; background-color: $core-white; border-radius: 16px; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; padding: $pad-xxlarge; box-shadow: 0px 3px 0px rgba(226, 228, 234, 0.4); diff --git a/frontend/styles/global/_global.scss b/frontend/styles/global/_global.scss index f7270d2eae..0c900164ac 100644 --- a/frontend/styles/global/_global.scss +++ b/frontend/styles/global/_global.scss @@ -116,5 +116,5 @@ hr { margin-top: $pad-xlarge; margin-bottom: $pad-xlarge; border: none; - border-bottom: 1px solid $ui-fleet-blue-15; + border-bottom: 1px solid $ui-fleet-black-10; } diff --git a/frontend/styles/var/colors.scss b/frontend/styles/var/colors.scss index 68e5ab694a..18daff24f8 100644 --- a/frontend/styles/var/colors.scss +++ b/frontend/styles/var/colors.scss @@ -11,7 +11,7 @@ $core-dark-blue-grey: #506e92; $ui-fleet-black-75: #515774; $ui-fleet-black-50: #8b8fa2; $ui-fleet-black-25: #c5c7d1; -$ui-fleet-blue-15: #e2e4ea; +$ui-fleet-black-10: #e2e4ea; $ui-fleet-blue-10: #F9FAFC; $ui-dark-blue-gray: #afbec1; $ui-blue-gray: #dbe3e5; @@ -48,8 +48,9 @@ $gradients-bright-gradient: linear-gradient(180deg, #ae6ddf 0%, #6a67fe 100%); // Colors for over (hover) and down (active) buttons styles $core-vibrant-red-over: #e93661; $core-vibrant-red-down: #cb3559; -$core-vibrant-blue-over: #5855eb; -$core-vibrant-blue-down: #3f3cd4; +$core-vibrant-blue-over: #5d5ae7; +$core-vibrant-blue-down: #4b4ab4; +$core-focused-outline: #d9d9fe; $core-fleet-blue-over: #303860; $core-fleet-blue-down: $core-fleet-black; diff --git a/frontend/styles/var/colors.ts b/frontend/styles/var/colors.ts index 19a803e072..32c2281d79 100644 --- a/frontend/styles/var/colors.ts +++ b/frontend/styles/var/colors.ts @@ -13,7 +13,7 @@ export const COLORS = { "ui-fleet-black-50": "#8B8FA2", "ui-fleet-black-33": "#B3B6C1", "ui-fleet-black-25": "#C5C7D1", - "ui-fleet-black-15": "#E2E4EA", + "ui-fleet-black-10": "#E2E4EA", "ui-off-white": "#F9FAFC", "ui-blue-hover": "#5D5AE7", "ui-blue-pressed": "#4B4AB4", From 97902a779fe1b13d6729336ae1410d35ac97dd45 Mon Sep 17 00:00:00 2001 From: Gabriel Hernandez Date: Thu, 5 Jan 2023 18:00:40 +0000 Subject: [PATCH 6/6] small style fixes after main merge --- frontend/pages/admin/AppSettingsPage/_styles.scss | 4 ++-- .../cards/Integrations/Integrations.tsx | 1 + .../IntegrationsPage/cards/Integrations/_styles.scss | 12 +++++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/frontend/pages/admin/AppSettingsPage/_styles.scss b/frontend/pages/admin/AppSettingsPage/_styles.scss index 57299e3a7b..df4d585b10 100644 --- a/frontend/pages/admin/AppSettingsPage/_styles.scss +++ b/frontend/pages/admin/AppSettingsPage/_styles.scss @@ -76,7 +76,7 @@ font-size: $medium; font-weight: $regular; color: $core-fleet-black; - border-bottom: solid 1px $ui-fleet-blue-15; + border-bottom: solid 1px $ui-fleet-black-10; margin: 0 0 $pad-xxlarge; @media (min-width: $break-990) { @@ -199,7 +199,7 @@ border-radius: 20%; height: 120px; width: 120px; - border: 1px solid $ui-fleet-blue-15; + border: 1px solid $ui-fleet-black-10; background-color: $ui-light-grey; position: relative; bottom: -20px; diff --git a/frontend/pages/admin/IntegrationsPage/cards/Integrations/Integrations.tsx b/frontend/pages/admin/IntegrationsPage/cards/Integrations/Integrations.tsx index 407b15f076..d1dc28ad6a 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/Integrations/Integrations.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/Integrations/Integrations.tsx @@ -398,6 +398,7 @@ const Integrations = (): JSX.Element => { return (
+

Ticket Destinations

Add or edit integrations to create tickets when Fleet detects new vulnerabilities. diff --git a/frontend/pages/admin/IntegrationsPage/cards/Integrations/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/Integrations/_styles.scss index e673705c12..7490a34df6 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/Integrations/_styles.scss +++ b/frontend/pages/admin/IntegrationsPage/cards/Integrations/_styles.scss @@ -6,7 +6,7 @@ font-size: $medium; font-weight: $regular; color: $core-fleet-black; - border-bottom: solid 1px $ui-fleet-blue-15; + border-bottom: solid 1px $ui-fleet-black-10; margin: 0 0 $pad-xxlarge; @media (min-width: $break-990) { @@ -14,6 +14,16 @@ } } + &__page-description { + font-size: $x-small; + color: $core-fleet-black; + width: 100%; + + @media (min-width: $break-990) { + width: 60%; + } + } + .table-container { @media (min-width: $break-990) { max-width: 65%;

-
- No Policies -
-

- - {currentTeam ? ( - <> - Ask yes or no questions about hosts assigned to{" "} - - {currentTeam.name} - - . - - ) : ( - <> - Ask yes or no questions about{" "} - all your hosts. - - )} - -

-
-

- - Verify whether or not your hosts have security features turned - on. -
- Track your efforts to keep installed software up to date - on your hosts. -
- Provide owners with a list of hosts that still need - changes. -

-
- {canAddOrDeletePolicy && ( -
- -
- )} -
-
-