Fleet UI: Side bar restyled, update UI for Team/Roles to match Figma (#1659)
* Style all settings side panels * Add builtin label icons * Update tests aligning jest userStub and adminUserStub * Update tests adding cypress checks for user teams/roles
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 691 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 353 B |
Binary file not shown.
|
After Width: | Height: | Size: 1013 B |
Binary file not shown.
|
After Width: | Height: | Size: 409 B |
@@ -0,0 +1 @@
|
||||
* Side panels in UI have a fresher look, Teams/Roles UI greyed out conditionally
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 <span>{formatter(val)}</span>;
|
||||
return <span className={greyed}>{formatter(val)}</span>;
|
||||
};
|
||||
|
||||
export default TextCell;
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
<h3 className="title">Labels</h3>
|
||||
<div className="title">
|
||||
<div>
|
||||
<h3>Labels</h3>
|
||||
</div>
|
||||
<div>
|
||||
{canAddNewLabel && (
|
||||
<Button
|
||||
variant="text-icon"
|
||||
onClick={onAddLabelClick}
|
||||
className={`${baseClass}__add-label-btn`}
|
||||
>
|
||||
Add label <img src={PlusIcon} alt="Add label icon" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`${baseClass}__panel-group-item ${baseClass}__panel-group-item--filter`}
|
||||
>
|
||||
@@ -89,15 +104,6 @@ class HostSidePanel extends Component {
|
||||
selectedFilter={selectedFilter}
|
||||
type="label"
|
||||
/>
|
||||
{canAddNewLabel ? (
|
||||
<Button
|
||||
variant="grey"
|
||||
onClick={onAddLabelClick}
|
||||
className={`${baseClass}__add-label-btn`}
|
||||
>
|
||||
Add new label
|
||||
</Button>
|
||||
) : null}
|
||||
</SecondarySidePanelContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 <img src={darwinIcon} alt="Apple icon" />;
|
||||
case "Linux":
|
||||
return <img src={linuxIcon} alt="Linux icon" />;
|
||||
case "Ubuntu Linux":
|
||||
return <img src={ubuntuIcon} alt="Ubuntu icon" />;
|
||||
case "CentOS Linux":
|
||||
return <img src={centosIcon} alt="Centos icon" />;
|
||||
case "Windows":
|
||||
return <img src={windowsIcon} alt="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 (
|
||||
<button className={wrapperClassName} onClick={onLabelClick}>
|
||||
<div className={`${baseClass}__flexy`}>
|
||||
<span className={`${baseClass}__name`}>{displayText}</span>
|
||||
<span className={`${baseClass}__name`}>
|
||||
{label_type === "builtin" && displayIcon(name)}
|
||||
{displayText}
|
||||
</span>
|
||||
<span className={`${baseClass}__count`}>{displayCount()}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+4
-4
@@ -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", () => {
|
||||
<QueryDetailsSidePanel
|
||||
onEditQuery={noop}
|
||||
query={queryStub}
|
||||
currentUser={userStub}
|
||||
currentUser={adminUserStub}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -23,7 +23,7 @@ describe("QueryDetailsSidePanel - component", () => {
|
||||
<QueryDetailsSidePanel
|
||||
onEditQuery={noop}
|
||||
query={queryStub}
|
||||
currentUser={userStub}
|
||||
currentUser={adminUserStub}
|
||||
/>
|
||||
);
|
||||
const aceEditor = component.find("FleetAce");
|
||||
@@ -39,7 +39,7 @@ describe("QueryDetailsSidePanel - component", () => {
|
||||
<QueryDetailsSidePanel
|
||||
onEditQuery={spy}
|
||||
query={queryStub}
|
||||
currentUser={userStub}
|
||||
currentUser={adminUserStub}
|
||||
/>
|
||||
);
|
||||
const button = component.find("Button");
|
||||
|
||||
@@ -91,7 +91,7 @@ class QuerySidePanel extends Component {
|
||||
return (
|
||||
<SecondarySidePanelContainer className={baseClass}>
|
||||
<div className={`${baseClass}__choose-table`}>
|
||||
<h2 className={`${baseClass}__header`}>Documentation</h2>
|
||||
<h2 className={`${baseClass}__header`}>Tables</h2>
|
||||
{renderTableSelect()}
|
||||
<p className={`${baseClass}__description`}>{description}</p>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.schedule-query-side-panel {
|
||||
padding: 20px;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
/>
|
||||
</div>
|
||||
<div className={`${baseClass}__additional body-wrap`}>
|
||||
<h2>Photo</h2>
|
||||
|
||||
<div className={`${baseClass}__change-avatar`}>
|
||||
<Avatar user={user} className={`${baseClass}__avatar`} />
|
||||
<a href="http://en.gravatar.com/emails/">
|
||||
Change photo at Gravatar
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{isBasicTier && (
|
||||
<div className={`${baseClass}__more-info-detail`}>
|
||||
<p className={`${baseClass}__header`}>Teams</p>
|
||||
<p
|
||||
className={`${baseClass}__description ${baseClass}__teams ${greyCell(
|
||||
teamsText
|
||||
)}`}
|
||||
>
|
||||
{teamsText}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className={`${baseClass}__more-info-detail`}>
|
||||
<p className={`${baseClass}__header`}>Role</p>
|
||||
<p className={`${baseClass}__description ${baseClass}__role`}>
|
||||
<p
|
||||
className={`${baseClass}__description ${baseClass}__role ${greyCell(
|
||||
roleText
|
||||
)}`}
|
||||
>
|
||||
{roleText}
|
||||
</p>
|
||||
</div>
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
<UserSettingsPage dispatch={noop} user={userStub} config={configStub} />
|
||||
);
|
||||
const pageWithAdmin = mount(
|
||||
<UserSettingsPage dispatch={noop} user={admin} config={configStub} />
|
||||
<UserSettingsPage
|
||||
dispatch={noop}
|
||||
user={adminUserStub}
|
||||
config={configStub}
|
||||
/>
|
||||
);
|
||||
|
||||
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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => <TextCell value={cellProps.cell.value} />,
|
||||
Cell: (cellProps) => (
|
||||
<TextCell
|
||||
value={cellProps.cell.value}
|
||||
greyed={greyCell(cellProps.cell.value)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Actions",
|
||||
@@ -114,7 +118,12 @@ const generateTableHeaders = (
|
||||
Header: "Teams",
|
||||
accessor: "teams",
|
||||
disableSortBy: true,
|
||||
Cell: (cellProps) => <TextCell value={cellProps.cell.value} />,
|
||||
Cell: (cellProps) => (
|
||||
<TextCell
|
||||
value={cellProps.cell.value}
|
||||
greyed={greyCell(cellProps.cell.value)}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -123,5 +123,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.grey-cell {
|
||||
color: $ui-fleet-black-25;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ describe("userManagementHelpers module", () => {
|
||||
|
||||
expect(updatedData).toEqual({
|
||||
email: "newemail@test.com",
|
||||
global_role: "admin",
|
||||
teams: [updatedTeam, newTeam],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(<QueryPage {...props} />);
|
||||
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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
+11
-6
@@ -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 }],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user