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:
RachelElysia
2021-08-16 12:25:51 -04:00
committed by GitHub
parent 57ce0d4bfd
commit f6091509f4
37 changed files with 315 additions and 133 deletions
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

+1
View File
@@ -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")
+8
View File
@@ -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);
});
});
+8 -2
View File
@@ -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);
});
});
+9 -1
View File
@@ -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);
});
});
+9 -1
View File
@@ -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)}&nbsp;
{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;
}
@@ -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;
+78
View File
@@ -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"
+10 -5
View File
@@ -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 });
+2 -2
View File
@@ -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
View File
@@ -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 }],
};