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

;
+ case "Linux":
+ return

;
+ case "Ubuntu Linux":
+ return

;
+ case "CentOS Linux":
+ return

;
+ case "Windows":
+ return

;
+ 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
-
-
+ {isBasicTier && (
+
+
Teams
+
+ {teamsText}
+
+
+ )}
@@ -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 }],
};