diff --git a/assets/images/icon-centos-fleet-black-16x16@2x.png b/assets/images/icon-centos-fleet-black-16x16@2x.png new file mode 100644 index 0000000000..011b0f4afd Binary files /dev/null and b/assets/images/icon-centos-fleet-black-16x16@2x.png differ diff --git a/assets/images/icon-darwin-fleet-black-16x16@2x.png b/assets/images/icon-darwin-fleet-black-16x16@2x.png new file mode 100644 index 0000000000..4e2c79585c Binary files /dev/null and b/assets/images/icon-darwin-fleet-black-16x16@2x.png differ diff --git a/assets/images/icon-linux-fleet-black-16x16@2x.png b/assets/images/icon-linux-fleet-black-16x16@2x.png new file mode 100644 index 0000000000..2e2b7b2c1e Binary files /dev/null and b/assets/images/icon-linux-fleet-black-16x16@2x.png differ diff --git a/assets/images/icon-plus-16x16@2x.png b/assets/images/icon-plus-16x16@2x.png new file mode 100644 index 0000000000..6fa0f04033 Binary files /dev/null and b/assets/images/icon-plus-16x16@2x.png differ diff --git a/assets/images/icon-ubuntu-fleet-black-16x16@2x.png b/assets/images/icon-ubuntu-fleet-black-16x16@2x.png new file mode 100644 index 0000000000..0e493c2cf9 Binary files /dev/null and b/assets/images/icon-ubuntu-fleet-black-16x16@2x.png differ diff --git a/assets/images/icon-windows-fleet-black-16x16@2x.png b/assets/images/icon-windows-fleet-black-16x16@2x.png new file mode 100644 index 0000000000..e3233d8a4c Binary files /dev/null and b/assets/images/icon-windows-fleet-black-16x16@2x.png differ diff --git a/changes/1659-side-bar-restyling b/changes/1659-side-bar-restyling new file mode 100644 index 0000000000..4aa964d96e --- /dev/null +++ b/changes/1659-side-bar-restyling @@ -0,0 +1 @@ +* Side panels in UI have a fresher look, Teams/Roles UI greyed out conditionally \ No newline at end of file diff --git a/cypress/integration/all/app/labelflow.spec.ts b/cypress/integration/all/app/labelflow.spec.ts index a84becdf7c..f4e04b3997 100644 --- a/cypress/integration/all/app/labelflow.spec.ts +++ b/cypress/integration/all/app/labelflow.spec.ts @@ -7,7 +7,7 @@ describe("Label flow", () => { it("Create, edit, and delete a label successfully", () => { cy.visit("/hosts/manage"); - cy.findByRole("button", { name: /add new label/i }).click(); + cy.findByRole("button", { name: /add label/i }).click(); // Using class selector because third party element doesn't work with Cypress Testing Selector Library cy.get(".ace_content") diff --git a/cypress/integration/basic/admin.spec.ts b/cypress/integration/basic/admin.spec.ts index 6fe5a4bbda..5b7a2f46c7 100644 --- a/cypress/integration/basic/admin.spec.ts +++ b/cypress/integration/basic/admin.spec.ts @@ -87,5 +87,13 @@ describe("Basic tier - Admin user", () => { }); cy.findByRole("button", { name: /create user/i }).click(); cy.findByText(/assign teams/i).should("exist"); + + // On the Profile page, they should… + // See Global in the Team section and Admin in the Role section + cy.visit("/profile"); + cy.findByText(/team/i) + .next() + .contains(/global/i); + cy.findByText("Role").next().contains(/admin/i); }); }); diff --git a/cypress/integration/basic/maintainer.spec.ts b/cypress/integration/basic/maintainer.spec.ts index 2bf8476f34..54d92b482f 100644 --- a/cypress/integration/basic/maintainer.spec.ts +++ b/cypress/integration/basic/maintainer.spec.ts @@ -74,5 +74,15 @@ describe("Basic tier - Maintainer user", () => { // On the Packs pages (manage, new, and edit), they should… // On the Schedule pages (manage, new, and edit), they should… // ^^General maintainer functionality for packs page is being tested in core/maintainer.spec.ts + + // On the Profile page, they should… + // See Global in the Team section and Maintainer in the Role section + cy.visit("/profile"); + cy.findByText(/team/i) + .next() + .contains(/global/i); + cy.findByText("Role") + .next() + .contains(/maintainer/i); }); }); diff --git a/cypress/integration/basic/observer.spec.ts b/cypress/integration/basic/observer.spec.ts index 4f8968b405..6463789366 100644 --- a/cypress/integration/basic/observer.spec.ts +++ b/cypress/integration/basic/observer.spec.ts @@ -72,5 +72,15 @@ describe("Basic tier - Observer user", () => { cy.contains(".table-container .data-table__table th", "Team").should( "be.visible" ); + + // On the Profile page, they should… + // See Global in the Team section and Observer in the Role section + cy.visit("/profile"); + cy.findByText(/team/i) + .next() + .contains(/apples/i); + cy.findByText("Role") + .next() + .contains(/observer/i); }); }); diff --git a/cypress/integration/basic/team_maintainer_observer.spec.ts b/cypress/integration/basic/team_maintainer_observer.spec.ts index 6d25e39d00..2fbc9a32ca 100644 --- a/cypress/integration/basic/team_maintainer_observer.spec.ts +++ b/cypress/integration/basic/team_maintainer_observer.spec.ts @@ -37,7 +37,7 @@ describe("Basic tier - Team observer/maintainer user", () => { cy.visit("/schedule/manage"); cy.findByText(/you do not have permissions/i).should("exist"); - // NOT see and select "add new label" + // NOT see and select "add label" cy.findByRole("button", { name: /new label/i }).should("not.exist"); // On the Host details page, they should… @@ -186,5 +186,15 @@ describe("Basic tier - Team observer/maintainer user", () => { // ^^TODO modify for expected host count once hosts are seeded }); }); + + // On the Profile page, they should… + // See 2 Teams in the Team section and Various in the Role section + cy.visit("/profile"); + cy.findByText("Teams") + .next() + .contains(/2 teams/i); + cy.findByText("Role") + .next() + .contains(/various/i); }); }); diff --git a/cypress/integration/core/admin.spec.ts b/cypress/integration/core/admin.spec.ts index a0fd62d062..a4b429aeae 100644 --- a/cypress/integration/core/admin.spec.ts +++ b/cypress/integration/core/admin.spec.ts @@ -37,8 +37,8 @@ describe("Core tier - Admin user", () => { cy.contains(/team/i).should("not.exist"); cy.findByRole("button", { name: /done/i }).click(); - // See and select "add new label" - cy.findByRole("button", { name: /new label/i }).click(); + // See and select "add label" + cy.findByRole("button", { name: /add label/i }).click(); cy.findByRole("button", { name: /cancel/i }).click(); // On the Host details page, they should… @@ -112,5 +112,11 @@ describe("Core tier - Admin user", () => { cy.findByText(/team/i).should("not.exist"); cy.visit("/settings/teams"); cy.findByText(/you do not have permissions/i).should("exist"); + + // On the Profile page, they should… + // See Admin in Role section, and no Team section + cy.visit("/profile"); + cy.findByText(/teams/i).should("not.exist"); + cy.findByText("Role").next().contains(/admin/i); }); }); diff --git a/cypress/integration/core/maintainer.spec.ts b/cypress/integration/core/maintainer.spec.ts index e4613237a6..c9b81b6afb 100644 --- a/cypress/integration/core/maintainer.spec.ts +++ b/cypress/integration/core/maintainer.spec.ts @@ -31,7 +31,7 @@ describe("Core tier - Maintainer user", () => { cy.findByText("select a team").should("not.exist"); cy.contains("button", /done/i).click(); - cy.contains("button", /add new label/i).click(); + cy.contains("button", /add label/i).click(); cy.contains("button", /cancel/i).click(); // Host details page: No team UI, can delete and create new query @@ -122,5 +122,13 @@ describe("Core tier - Maintainer user", () => { // Schedule page: Can create, edit, remove a schedule // TODO: Copy flow from queryflow.spec.ts here to ensure maintainers have access + + // On the Profile page, they should… + // See Maintainer in Role section, and no Team section + cy.visit("/profile"); + cy.findByText(/teams/i).should("not.exist"); + cy.findByText("Role") + .next() + .contains(/maintainer/i); }); }); diff --git a/cypress/integration/core/observer.spec.ts b/cypress/integration/core/observer.spec.ts index bce7f913c5..c7fc303805 100644 --- a/cypress/integration/core/observer.spec.ts +++ b/cypress/integration/core/observer.spec.ts @@ -33,7 +33,7 @@ describe("Core tier - Observer user", () => { cy.visit("/hosts/manage"); cy.findByText(/teams/i).should("not.exist"); cy.contains("button", /add new host/i).should("not.exist"); - cy.contains("button", /add new label/i).should("not.exist"); + cy.contains("button", /add label/i).should("not.exist"); // Host details page: No team UI, cannot delete or query cy.get("tbody").within(() => { @@ -78,5 +78,13 @@ describe("Core tier - Observer user", () => { cy.findByText(/show sql/i).click(); cy.get(".target-select").should("not.exist"); cy.findByRole("button", { name: /run/i }).should("not.exist"); + + // On the Profile page, they should… + // See Observer in Role section, and no Team section + cy.visit("/profile"); + cy.findByText(/teams/i).should("not.exist"); + cy.findByText("Role") + .next() + .contains(/observer/i); }); }); diff --git a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx index 8480d1e0d9..5a0205e28c 100644 --- a/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx +++ b/frontend/components/TableContainer/DataTable/TextCell/TextCell.tsx @@ -3,12 +3,14 @@ import React from "react"; interface ITextCellProps { value: string | number | boolean; formatter?: (val: any) => string; + greyed?: string; } const TextCell = (props: ITextCellProps): JSX.Element => { const { value, formatter = (val) => val, // identity function if no formatter is provided + greyed, } = props; let val = value; @@ -17,7 +19,7 @@ const TextCell = (props: ITextCellProps): JSX.Element => { val = value.toString(); } - return {formatter(val)}; + return {formatter(val)}; }; export default TextCell; diff --git a/frontend/components/side_panels/HostSidePanel/HostSidePanel.jsx b/frontend/components/side_panels/HostSidePanel/HostSidePanel.jsx index 7f37a3d4d7..7574394d77 100644 --- a/frontend/components/side_panels/HostSidePanel/HostSidePanel.jsx +++ b/frontend/components/side_panels/HostSidePanel/HostSidePanel.jsx @@ -8,6 +8,7 @@ import labelInterface from "interfaces/label"; import PanelGroup from "components/side_panels/HostSidePanel/PanelGroup"; import SecondarySidePanelContainer from "components/side_panels/SecondarySidePanelContainer"; import statusLabelsInterface from "interfaces/status_labels"; +import PlusIcon from "../../../../assets/images/icon-plus-16x16@2x.png"; const baseClass = "host-side-panel"; @@ -70,8 +71,22 @@ class HostSidePanel extends Component { selectedFilter={selectedFilter} type="platform" /> - -

Labels

+
+
+

Labels

+
+
+ {canAddNewLabel && ( + + )} +
+
@@ -89,15 +104,6 @@ class HostSidePanel extends Component { selectedFilter={selectedFilter} type="label" /> - {canAddNewLabel ? ( - - ) : null} ); } diff --git a/frontend/components/side_panels/HostSidePanel/PanelGroupItem/PanelGroupItem.jsx b/frontend/components/side_panels/HostSidePanel/PanelGroupItem/PanelGroupItem.jsx index e8bd8ef4af..4e2472b80a 100644 --- a/frontend/components/side_panels/HostSidePanel/PanelGroupItem/PanelGroupItem.jsx +++ b/frontend/components/side_panels/HostSidePanel/PanelGroupItem/PanelGroupItem.jsx @@ -3,9 +3,30 @@ import PropTypes from "prop-types"; import classnames from "classnames"; import statusLabelsInterface from "interfaces/status_labels"; +import darwinIcon from "../../../../../assets/images/icon-darwin-fleet-black-16x16@2x.png"; +import linuxIcon from "../../../../../assets/images/icon-linux-fleet-black-16x16@2x.png"; +import ubuntuIcon from "../../../../../assets/images/icon-ubuntu-fleet-black-16x16@2x.png"; +import centosIcon from "../../../../../assets/images/icon-centos-fleet-black-16x16@2x.png"; +import windowsIcon from "../../../../../assets/images/icon-windows-fleet-black-16x16@2x.png"; const baseClass = "panel-group-item"; +const displayIcon = (name) => { + switch (name) { + case "Darwin": + return Apple icon; + case "Linux": + return Linux icon; + case "Ubuntu Linux": + return Ubuntu icon; + case "CentOS Linux": + return Centos icon; + case "Windows": + return Windows icon; + default: + break; + } +}; class PanelGroupItem extends Component { static propTypes = { item: PropTypes.shape({ @@ -15,6 +36,7 @@ class PanelGroupItem extends Component { type: PropTypes.string.isRequired, id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, name: PropTypes.string, + label_type: PropTypes.string, }).isRequired, onLabelClick: PropTypes.func, isSelected: PropTypes.bool, @@ -39,7 +61,8 @@ class PanelGroupItem extends Component { render() { const { displayCount } = this; const { item, onLabelClick, isSelected } = this.props; - const { display_text: displayText, type } = item; + const { display_text: displayText, type, label_type, name } = item; + const wrapperClassName = classnames( baseClass, "button", @@ -56,7 +79,10 @@ class PanelGroupItem extends Component { return ( diff --git a/frontend/components/side_panels/HostSidePanel/PanelGroupItem/_styles.scss b/frontend/components/side_panels/HostSidePanel/PanelGroupItem/_styles.scss index 3d398e58cd..7d256e5c6a 100644 --- a/frontend/components/side_panels/HostSidePanel/PanelGroupItem/_styles.scss +++ b/frontend/components/side_panels/HostSidePanel/PanelGroupItem/_styles.scss @@ -11,6 +11,13 @@ $base-class: "panel-group-item"; margin: 2px 0; border-radius: 8px; + img { + width: 16px; + height: 16px; + margin-right: $pad-xsmall; + vertical-align: sub; + } + &.#{$base-class}--selected { font-weight: $bold; background-color: $ui-vibrant-blue-25; diff --git a/frontend/components/side_panels/HostSidePanel/_styles.scss b/frontend/components/side_panels/HostSidePanel/_styles.scss index e61c02f3cf..57ef421461 100644 --- a/frontend/components/side_panels/HostSidePanel/_styles.scss +++ b/frontend/components/side_panels/HostSidePanel/_styles.scss @@ -1,6 +1,5 @@ .host-side-panel { color: $core-fleet-black; - padding: 14px 20px 22px; .input-icon-field { margin-top: 0; @@ -22,8 +21,12 @@ } &__add-label-btn { - width: 100%; - margin-top: $pad-large; + color: $core-vibrant-blue !important; + align-self: right; + + img { + margin-left: $pad-xsmall; + } } &__panel-group-item { @@ -44,10 +47,17 @@ left: 8px; } } + } - .title { - padding-left: 20px; - font-size: $small; + .title { + font-size: $small; + display: flex; + justify-content: space-between; + + button { + position: relative; + top: 12px; + padding: 0 $pad-xsmall; } } diff --git a/frontend/components/side_panels/PackInfoSidePanel/_styles.scss b/frontend/components/side_panels/PackInfoSidePanel/_styles.scss index caf803c89a..a03b4e792f 100644 --- a/frontend/components/side_panels/PackInfoSidePanel/_styles.scss +++ b/frontend/components/side_panels/PackInfoSidePanel/_styles.scss @@ -4,7 +4,6 @@ border-left: 1px solid $ui-fleet-blue-15; box-sizing: border-box; overflow: scroll; - padding: $pad-xlarge; &__title { font-size: $medium; @@ -16,7 +15,7 @@ } &__subtitle { - font-size: $small; + font-size: $x-small; font-weight: $bold; color: $core-fleet-black; } diff --git a/frontend/components/side_panels/QueryDetailsSidePanel/QueryDetailsSidePanel.tests.jsx b/frontend/components/side_panels/QueryDetailsSidePanel/QueryDetailsSidePanel.tests.jsx index 4b4941bae8..7f4dd6518a 100644 --- a/frontend/components/side_panels/QueryDetailsSidePanel/QueryDetailsSidePanel.tests.jsx +++ b/frontend/components/side_panels/QueryDetailsSidePanel/QueryDetailsSidePanel.tests.jsx @@ -3,7 +3,7 @@ import { mount } from "enzyme"; import { noop } from "lodash"; import QueryDetailsSidePanel from "components/side_panels/QueryDetailsSidePanel"; -import { queryStub, userStub } from "test/stubs"; +import { queryStub, adminUserStub } from "test/stubs"; describe("QueryDetailsSidePanel - component", () => { it("renders", () => { @@ -11,7 +11,7 @@ describe("QueryDetailsSidePanel - component", () => { ); @@ -23,7 +23,7 @@ describe("QueryDetailsSidePanel - component", () => { ); const aceEditor = component.find("FleetAce"); @@ -39,7 +39,7 @@ describe("QueryDetailsSidePanel - component", () => { ); const button = component.find("Button"); diff --git a/frontend/components/side_panels/QuerySidePanel/QuerySidePanel.jsx b/frontend/components/side_panels/QuerySidePanel/QuerySidePanel.jsx index f1bfc3229a..0d67f14d09 100644 --- a/frontend/components/side_panels/QuerySidePanel/QuerySidePanel.jsx +++ b/frontend/components/side_panels/QuerySidePanel/QuerySidePanel.jsx @@ -91,7 +91,7 @@ class QuerySidePanel extends Component { return (
-

Documentation

+

Tables

{renderTableSelect()}

{description}

diff --git a/frontend/components/side_panels/QuerySidePanel/_styles.scss b/frontend/components/side_panels/QuerySidePanel/_styles.scss index c65d030227..5f6e8a6850 100644 --- a/frontend/components/side_panels/QuerySidePanel/_styles.scss +++ b/frontend/components/side_panels/QuerySidePanel/_styles.scss @@ -1,6 +1,4 @@ .query-side-panel { - padding: 0 $pad-large; - &__header { margin: 0 0 $pad-medium; font-size: $x-small; @@ -9,7 +7,7 @@ } &__choose-table { - margin: $pad-xxlarge 0 $pad-medium; + margin: 0 0 $pad-medium; .form-field { margin-bottom: $pad-medium; diff --git a/frontend/components/side_panels/ScheduleQuerySidePanel/_styles.scss b/frontend/components/side_panels/ScheduleQuerySidePanel/_styles.scss deleted file mode 100644 index 68f654b039..0000000000 --- a/frontend/components/side_panels/ScheduleQuerySidePanel/_styles.scss +++ /dev/null @@ -1,3 +0,0 @@ -.schedule-query-side-panel { - padding: 20px; -} diff --git a/frontend/components/side_panels/SecondarySidePanelContainer/_styles.scss b/frontend/components/side_panels/SecondarySidePanelContainer/_styles.scss index 9e6716f141..8ee7ef38aa 100644 --- a/frontend/components/side_panels/SecondarySidePanelContainer/_styles.scss +++ b/frontend/components/side_panels/SecondarySidePanelContainer/_styles.scss @@ -1,10 +1,11 @@ .secondary-side-panel-container { align-self: stretch; - background-color: $ui-off-white; + background-color: $core-white; box-sizing: border-box; border-left: 1px solid $ui-gray; overflow: auto; width: $sidepanel-width; + padding: $pad-xxlarge; @include breakpoint(smalldesk) { width: $sidepanel-tablet-width; diff --git a/frontend/fleet/helpers.ts b/frontend/fleet/helpers.ts index b619dc02a4..7a3853e8a5 100644 --- a/frontend/fleet/helpers.ts +++ b/frontend/fleet/helpers.ts @@ -2,6 +2,8 @@ import { flatMap, omit, pick, size } from "lodash"; import md5 from "js-md5"; import moment from "moment"; import yaml from "js-yaml"; +import stringUtils from "utilities/strings"; +import { ITeam } from "interfaces/team"; const ORG_INFO_ATTRS = ["org_name", "org_logo_url"]; const ADMIN_ATTRS = ["email", "name", "password", "password_confirmation"]; @@ -410,6 +412,79 @@ export const formatPackForClient = (pack: any): any => { return pack; }; +export const generateRole = ( + teams: ITeam[], + globalRole: string | null +): string => { + if (globalRole === null) { + const listOfRoles: (string | undefined)[] = teams.map((team) => team.role); + + if (teams.length === 0) { + // no global role and no teams + return "Unassigned"; + } else if (teams.length === 1) { + // no global role and only one team + return stringUtils.capitalize(teams[0].role ?? ""); + } else if ( + listOfRoles.every( + (role: string | undefined): boolean => role === "maintainer" + ) + ) { + // only team maintainers + return "Maintainer"; + } else if ( + listOfRoles.every( + (role: string | undefined): boolean => role === "observer" + ) + ) { + // only team observers + return "Observer"; + } + + return "Various"; // no global role and multiple teams + } + + if (teams.length === 0) { + // global role and no teams + return stringUtils.capitalize(globalRole); + } + return "Various"; // global role and one or more teams +}; + +export const generateTeam = ( + teams: ITeam[], + globalRole: string | null +): string => { + if (globalRole === null) { + if (teams.length === 0) { + // no global role and no teams + return "No Team"; + } else if (teams.length === 1) { + // no global role and only one team + return teams[0].name; + } + return `${teams.length} teams`; // no global role and multiple teams + } + + if (teams.length === 0) { + // global role and no teams + return "Global"; + } + return `${teams.length + 1} teams`; // global role and one or more teams +}; + +export const greyCell = (roleOrTeamText: string): string => { + const GREYED_TEXT = ["Global", "Unassigned", "Various", "No Team"]; + + if ( + GREYED_TEXT.includes(roleOrTeamText) || + roleOrTeamText.includes(" teams") + ) { + return "grey-cell"; + } + return ""; +}; + const setupData = (formData: any) => { const orgInfo = pick(formData, ORG_INFO_ATTRS); const adminInfo = pick(formData, ADMIN_ATTRS); @@ -550,6 +625,9 @@ export default { formatTeamScheduledQueryForClient, formatTeamScheduledQueryForServer, formatSelectedTargetsForApi, + generateRole, + generateTeam, + greyCell, humanHostUptime, humanHostLastSeen, humanHostEnrolled, diff --git a/frontend/pages/UserSettingsPage/UserSettingsPage.jsx b/frontend/pages/UserSettingsPage/UserSettingsPage.jsx index 6cbaa4b722..93a6c3eeba 100644 --- a/frontend/pages/UserSettingsPage/UserSettingsPage.jsx +++ b/frontend/pages/UserSettingsPage/UserSettingsPage.jsx @@ -13,6 +13,7 @@ import Button from "components/buttons/Button"; import ChangeEmailForm from "components/forms/ChangeEmailForm"; import ChangePasswordForm from "components/forms/ChangePasswordForm"; import deepDifference from "utilities/deep_difference"; +import permissionUtils from "utilities/permissions"; import FleetIcon from "components/icons/FleetIcon"; import InputField from "components/forms/fields/InputField"; import { logoutUser, updateUser } from "redux/nodes/auth/actions"; @@ -24,6 +25,7 @@ import userActions from "redux/nodes/entities/users/actions"; import versionActions from "redux/nodes/version/actions"; import userInterface from "interfaces/user"; import UserSettingsForm from "components/forms/UserSettingsForm"; +import { generateRole, generateTeam, greyCell } from "fleet/helpers"; const baseClass = "user-settings"; @@ -42,6 +44,7 @@ export class UserSettingsPage extends Component { new_password: PropTypes.string, old_password: PropTypes.string, }), + isBasicTier: PropTypes.bool, }; static defaultProps = { @@ -316,15 +319,23 @@ export class UserSettingsPage extends Component { renderPasswordModal, renderApiTokenModal, } = this; - const { version, errors, user, config } = this.props; + const { version, errors, user, config, isBasicTier } = this.props; const { pendingEmail } = this.state; if (!user) { return false; } - const { admin, updated_at: updatedAt, sso_enabled: ssoEnabled } = user; - const roleText = admin ? "Admin" : "User"; + const { + global_role: globalRole, + updated_at: updatedAt, + sso_enabled: ssoEnabled, + teams, + } = user; + + const roleText = generateRole(teams, globalRole); + const teamsText = generateTeam(teams, globalRole); + const lastUpdatedAt = moment(updatedAt).fromNow(); return ( @@ -341,18 +352,31 @@ export class UserSettingsPage extends Component { />
-

Photo

-
Change photo at Gravatar
- + {isBasicTier && ( +
+

Teams

+

+ {teamsText} +

+
+ )}

Role

-

+

{roleText}

@@ -392,8 +416,9 @@ const mapStateToProps = (state) => { const { errors, user } = state.auth; const { config } = state.app; const { errors: userErrors } = state.entities.users; + const isBasicTier = permissionUtils.isBasicTier(config); - return { version, errors, user, userErrors, config }; + return { version, errors, user, userErrors, config, isBasicTier }; }; export default connect(mapStateToProps)(UserSettingsPage); diff --git a/frontend/pages/UserSettingsPage/UserSettingsPage.tests.jsx b/frontend/pages/UserSettingsPage/UserSettingsPage.tests.jsx index 2e454c06a9..34471e2272 100644 --- a/frontend/pages/UserSettingsPage/UserSettingsPage.tests.jsx +++ b/frontend/pages/UserSettingsPage/UserSettingsPage.tests.jsx @@ -6,7 +6,7 @@ import ConnectedPage, { UserSettingsPage, } from "pages/UserSettingsPage/UserSettingsPage"; import testHelpers from "test/helpers"; -import { userStub, configStub } from "test/stubs"; +import { userStub, configStub, adminUserStub } from "test/stubs"; import * as authActions from "redux/nodes/auth/actions"; const { connectedComponent, fillInFormInput, reduxMockStore } = testHelpers; @@ -27,20 +27,19 @@ describe("UserSettingsPage - component", () => { }); it("contains expected text", () => { - const admin = { ...userStub, admin: true }; const pageWithUser = mount( ); const pageWithAdmin = mount( - + ); - expect(pageWithUser.find(".user-settings__role").text()).toContain("User"); - expect(pageWithUser.find(".user-settings__role").text()).not.toContain( - "Admin" - ); - expect(pageWithAdmin.find(".user-settings__role").text()).not.toContain( - "User" + expect(pageWithUser.find(".user-settings__role").text()).toContain( + "Observer" ); expect(pageWithAdmin.find(".user-settings__role").text()).toContain( "Admin" diff --git a/frontend/pages/UserSettingsPage/_styles.scss b/frontend/pages/UserSettingsPage/_styles.scss index a67bc0bc91..6b0ffb357e 100644 --- a/frontend/pages/UserSettingsPage/_styles.scss +++ b/frontend/pages/UserSettingsPage/_styles.scss @@ -17,13 +17,14 @@ } &__additional { - width: 350px; - max-width: 350px; - min-width: 350px; + width: 340px; + max-width: 340px; + min-width: 340px; box-sizing: border-box; - padding: $pad-xlarge; - background-color: $ui-light-grey; + padding: $pad-xxlarge; + background-color: $core-white; min-height: 100vh; + border-left: solid 1px $ui-fleet-blue-15; } &__change-avatar { @@ -157,4 +158,8 @@ display: block; margin-top: $pad-xxlarge; } + + .grey-cell { + color: $ui-fleet-black-25; + } } diff --git a/frontend/pages/admin/UserManagementPage/UsersTableConfig.tsx b/frontend/pages/admin/UserManagementPage/UsersTableConfig.tsx index 5b5051f4ab..d80bced4dd 100644 --- a/frontend/pages/admin/UserManagementPage/UsersTableConfig.tsx +++ b/frontend/pages/admin/UserManagementPage/UsersTableConfig.tsx @@ -5,9 +5,8 @@ import StatusCell from "components/TableContainer/DataTable/StatusCell/StatusCel import TextCell from "components/TableContainer/DataTable/TextCell/TextCell"; import { IInvite } from "interfaces/invite"; import { IUser } from "interfaces/user"; -import { ITeam } from "interfaces/team"; import { IDropdownOption } from "interfaces/dropdownOption"; -import stringUtils from "utilities/strings"; +import { generateRole, generateTeam, greyCell } from "fleet/helpers"; import DropdownCell from "../../../components/TableContainer/DataTable/DropdownCell"; interface IHeaderProps { @@ -88,7 +87,12 @@ const generateTableHeaders = ( Header: "Roles", accessor: "roles", disableSortBy: true, - Cell: (cellProps) => , + Cell: (cellProps) => ( + + ), }, { title: "Actions", @@ -114,7 +118,12 @@ const generateTableHeaders = ( Header: "Teams", accessor: "teams", disableSortBy: true, - Cell: (cellProps) => , + Cell: (cellProps) => ( + + ), }); } @@ -131,57 +140,6 @@ const generateStatus = (type: string, data: IUser | IInvite): string => { return type === "invite" ? "Invite pending" : "Active"; }; -const generateTeam = (teams: ITeam[], globalRole: string | null): string => { - if (globalRole === null) { - if (teams.length === 0) { - // no global role and no teams - return "No Team"; - } else if (teams.length === 1) { - // no global role and only one team - return teams[0].name; - } - return `${teams.length} teams`; // no global role and multiple teams - } - - if (teams.length === 0) { - // global role and no teams - return "Global"; - } - return `${teams.length + 1} teams`; // global role and one or more teams -}; - -const generateRole = (teams: ITeam[], globalRole: string | null): string => { - if (globalRole === null) { - const listOfRoles: any = teams.map((team) => team.role); - - if (teams.length === 0) { - // no global role and no teams - return "Unassigned"; - } else if (teams.length === 1) { - // no global role and only one team - return stringUtils.capitalize(teams[0].role ?? ""); - } else if ( - listOfRoles.every((role: string): boolean => role === "maintainer") - ) { - // only team maintainers - return stringUtils.capitalize(teams[0].role ?? ""); - } else if ( - listOfRoles.every((role: string): boolean => role === "observer") - ) { - // only team observers - return stringUtils.capitalize(teams[0].role ?? ""); - } - - return "Various"; // no global role and multiple teams - } - - if (teams.length === 0) { - // global role and no teams - return stringUtils.capitalize(globalRole); - } - return "Various"; // global role and one or more teams -}; - const generateActionDropdownOptions = ( isCurrentUser: boolean, isInvitePending: boolean, diff --git a/frontend/pages/admin/UserManagementPage/_styles.scss b/frontend/pages/admin/UserManagementPage/_styles.scss index 99d52318e4..52d6db6eef 100644 --- a/frontend/pages/admin/UserManagementPage/_styles.scss +++ b/frontend/pages/admin/UserManagementPage/_styles.scss @@ -123,5 +123,9 @@ } } } + + .grey-cell { + color: $ui-fleet-black-25; + } } } diff --git a/frontend/pages/admin/UserManagementPage/helpers/userManagementHelpers.tests.ts b/frontend/pages/admin/UserManagementPage/helpers/userManagementHelpers.tests.ts index c290778a73..7f66178eee 100644 --- a/frontend/pages/admin/UserManagementPage/helpers/userManagementHelpers.tests.ts +++ b/frontend/pages/admin/UserManagementPage/helpers/userManagementHelpers.tests.ts @@ -30,6 +30,7 @@ describe("userManagementHelpers module", () => { expect(updatedData).toEqual({ email: "newemail@test.com", + global_role: "admin", teams: [updatedTeam, newTeam], }); }); diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx index 3a5cc56c67..3e58e4f209 100644 --- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx +++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tests.jsx @@ -13,7 +13,7 @@ import { reduxMockStore, stubbedOsqueryTable, } from "test/helpers"; -import { hostStub, configStub, userStub, teamStub } from "test/stubs"; +import { hostStub, configStub, adminUserStub, teamStub } from "test/stubs"; import * as manageHostsPageActions from "redux/nodes/components/ManageHostsPage/actions"; const allHostsLabel = { @@ -48,7 +48,7 @@ const customLabel = { }; const mockStore = reduxMockStore({ app: { enrollSecret: [], config: {} }, - auth: { user: userStub }, + auth: { user: adminUserStub }, components: { ManageHostsPage: { display: "Grid", diff --git a/frontend/pages/queries/QueryPage/QueryPage.tests.jsx b/frontend/pages/queries/QueryPage/QueryPage.tests.jsx index 8ff6d4fbdb..4c161daefc 100644 --- a/frontend/pages/queries/QueryPage/QueryPage.tests.jsx +++ b/frontend/pages/queries/QueryPage/QueryPage.tests.jsx @@ -15,7 +15,7 @@ import { hostStub, queryStub, labelStub, - userStub, + adminUserStub, configStub, } from "test/stubs"; @@ -62,7 +62,7 @@ describe("QueryPage - component", () => { // THIS WAS ADDED 5/24, not sure if it's correct auth: { user: { - ...userStub, + ...adminUserStub, }, }, }; @@ -229,7 +229,7 @@ describe("QueryPage - component", () => { }, }, auth: { - user: userStub, + user: adminUserStub, }, }); const page = mount( @@ -280,7 +280,7 @@ describe("QueryPage - component", () => { query: { query: "select * from users" }, selectedOsqueryTable: defaultSelectedOsqueryTable, selectedTargets: [hostStub], - currentUser: userStub, + currentUser: adminUserStub, }; const Page = mount(); const PageNode = Page.instance(); @@ -324,7 +324,7 @@ describe("QueryPage - component", () => { dispatch={noop} query={queryStub} selectedOsqueryTable={defaultSelectedOsqueryTable} - currentUser={userStub} + currentUser={adminUserStub} /> ); const filename = "query_results.csv"; @@ -369,7 +369,7 @@ describe("QueryPage - component", () => { dispatch={noop} query={queryStub} selectedOsqueryTable={defaultSelectedOsqueryTable} - currentUser={userStub} + currentUser={adminUserStub} /> ); Page.setState({ campaign }); diff --git a/frontend/styles/var/_global.scss b/frontend/styles/var/_global.scss index a2d08e55cb..717f03124e 100644 --- a/frontend/styles/var/_global.scss +++ b/frontend/styles/var/_global.scss @@ -1,4 +1,4 @@ $border-radius: 4px; $base-font-size: 16; -$sidepanel-width: 300px; -$sidepanel-tablet-width: 300px; +$sidepanel-width: 340px; +$sidepanel-tablet-width: 340px; diff --git a/frontend/test/stubs.ts b/frontend/test/stubs.ts index c0acfb18c9..0b07a75902 100644 --- a/frontend/test/stubs.ts +++ b/frontend/test/stubs.ts @@ -3,9 +3,14 @@ import { ITeam } from "interfaces/team"; export const adminUserStub = { id: 1, - admin: true, email: "hi@gnar.dog", + force_password_reset: false, + api_only: false, + global_role: "admin", + gravatar_url: "https://image.com", name: "Gnar Mike", + sso_enabled: false, + teams: [], }; export const configStub = { @@ -196,12 +201,12 @@ export const userTeamStub: ITeam = { export const userStub: IUser = { id: 1, - email: "hi@gnar.dog", - force_password_reset: false, - api_only: false, - global_role: "admin", - gravatar_url: "https://image.com", name: "Gnar Mike", + email: "hi@gnar.dog", + global_role: null, + api_only: false, + force_password_reset: false, + gravatar_url: "https://image.com", sso_enabled: false, teams: [{ ...userTeamStub }], };