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 7d2cac45c9..0000000000 Binary files a/assets/images/no-matching-host-100x100@2x.png and /dev/null differ diff --git a/assets/images/no-policy-323x138@2x.png b/assets/images/no-policy-323x138@2x.png deleted file mode 100644 index 51affd1c71..0000000000 Binary files a/assets/images/no-policy-323x138@2x.png and /dev/null differ diff --git a/assets/images/no-policy.svg b/assets/images/no-policy.svg deleted file mode 100644 index 9ccd15f7fb..0000000000 --- a/assets/images/no-policy.svg +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/no-schedule-322x138@2x.png b/assets/images/no-schedule-322x138@2x.png deleted file mode 100644 index 3dac4cc4cc..0000000000 Binary files a/assets/images/no-schedule-322x138@2x.png and /dev/null differ diff --git a/assets/images/no-schedule.svg b/assets/images/no-schedule.svg deleted file mode 100644 index 01b0fa7a6b..0000000000 --- a/assets/images/no-schedule.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/images/robo-dog-176x144@2x.png b/assets/images/robo-dog-176x144@2x.png deleted file mode 100644 index b2859d3b80..0000000000 Binary files a/assets/images/robo-dog-176x144@2x.png and /dev/null differ 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/changes/issue-6799-software-empty-states b/changes/issue-6799-software-empty-states new file mode 100644 index 0000000000..fd6e891aa1 --- /dev/null +++ b/changes/issue-6799-software-empty-states @@ -0,0 +1 @@ +* Update software empty states \ No newline at end of file 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/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/cypress/integration/all/app/policiesflow.spec.ts b/cypress/integration/all/app/policiesflow.spec.ts index 4070d3fe0c..bfc981dc45 100644 --- a/cypress/integration/all/app/policiesflow.spec.ts +++ b/cypress/integration/all/app/policiesflow.spec.ts @@ -143,7 +143,7 @@ describe("Policies flow (empty)", () => { 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/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/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/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/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/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..101eadd268 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 { @@ -52,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; @@ -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/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/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/components/TableContainer/DataTable/_styles.scss b/frontend/components/TableContainer/DataTable/_styles.scss index d0fa604734..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, @@ -55,18 +55,25 @@ $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 + } } } tr { - border-bottom: 1px solid $ui-fleet-blue-15; + border-bottom: 1px solid $ui-fleet-black-10; &:last-child { border-bottom: 0; @@ -88,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 { @@ -109,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/TableContainer/TableContainer.tsx b/frontend/components/TableContainer/TableContainer.tsx index 9099a172a4..ae3953eccf 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 = ({
@@ -335,7 +338,6 @@ const TableContainer = ({ {customControl && customControl()}
-
{/* Render search bar only if not empty component */} {searchable && !wideSearch && ( @@ -350,6 +352,7 @@ const TableContainer = ({ >
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/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/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 d089de8086..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; @@ -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/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/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/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/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..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; @@ -92,9 +92,4 @@ color: $core-fleet-black; } } - - &__no-queries { - font-size: $x-small; - font-weight: $bold; - } } 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/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 a5c8ed7335..a2f8ec3771 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/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/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/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/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/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/admin/AppSettingsPage/_styles.scss b/frontend/pages/admin/AppSettingsPage/_styles.scss index 4eeecb2ddf..df4d585b10 100644 --- a/frontend/pages/admin/AppSettingsPage/_styles.scss +++ b/frontend/pages/admin/AppSettingsPage/_styles.scss @@ -49,6 +49,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"); @@ -75,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) { @@ -198,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 dea60d91ef..d1dc28ad6a 100644 --- a/frontend/pages/admin/IntegrationsPage/cards/Integrations/Integrations.tsx +++ b/frontend/pages/admin/IntegrationsPage/cards/Integrations/Integrations.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"; @@ -41,7 +43,7 @@ const BAD_REQUEST_ERROR = const UNKNOWN_ERROR = "We experienced an error when attempting to connect. Please try again later."; -const Integrations = () => { +const Integrations = (): JSX.Element => { const { renderFlash } = useContext(NotificationContext); const [showAddIntegrationModal, setShowAddIntegrationModal] = useState(false); @@ -361,35 +363,33 @@ const Integrations = () => { } }; - 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); @@ -399,6 +399,10 @@ const Integrations = () => { return (

Ticket Destinations

+

+ Add or edit integrations to create tickets when Fleet detects new + vulnerabilities. +

{loadingIntegrationsError ? ( ) : ( @@ -413,7 +417,15 @@ const Integrations = () => { 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/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%; diff --git a/frontend/pages/admin/IntegrationsPage/cards/Mdm/_styles.scss b/frontend/pages/admin/IntegrationsPage/cards/Mdm/_styles.scss index d634668b68..197a248f73 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; } 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..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; } } } @@ -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/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/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/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/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/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/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx index e9942c9a7d..c6dab52701 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/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/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/LabelFilterSelect/_styles.scss b/frontend/pages/hosts/ManageHostsPage/components/LabelFilterSelect/_styles.scss index 5c64c67658..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; @@ -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/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/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/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/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/hosts/details/cards/About/About.tsx b/frontend/pages/hosts/details/cards/About/About.tsx index 13c5a496d4..5d2884823d 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/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/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/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/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 ( -
-
- 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 && ( -
- -
- )} -
-
-
- ); + 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/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/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/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/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; 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/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 b10b7ad865..18daff24f8 100644 --- a/frontend/styles/var/colors.scss +++ b/frontend/styles/var/colors.scss @@ -11,7 +11,8 @@ $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; $ui-gray: #e3e3e3; @@ -47,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", 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 }); };