mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
Query Manage Page: Role based views (#843)
* Removes create new query button from only observers * Renders CTA ManageQueriesPage side panel * Renders Observer can run column for non observers * Fixes integration tests: ManageQueriesPage and SidePanel UI co-authored by: @gillespi314 Integration tests co-authored by: @ghernandez345
This commit is contained in:
parent
0395020b6f
commit
a6e921c7b8
@ -15,11 +15,11 @@ class KolideAce extends Component {
|
||||
static propTypes = {
|
||||
error: PropTypes.string,
|
||||
fontSize: PropTypes.number,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
handleSubmit: PropTypes.func,
|
||||
label: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onLoad: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
onLoad: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
readOnly: PropTypes.bool,
|
||||
showGutter: PropTypes.bool,
|
||||
|
@ -19,6 +19,7 @@ class QueriesList extends Component {
|
||||
onDblClickQuery: PropTypes.func,
|
||||
queries: PropTypes.arrayOf(queryInterface).isRequired,
|
||||
selectedQuery: queryInterface,
|
||||
isOnlyObserver: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -96,6 +97,7 @@ class QueriesList extends Component {
|
||||
onDblClickQuery,
|
||||
queries,
|
||||
selectedQuery,
|
||||
isOnlyObserver,
|
||||
} = this.props;
|
||||
const { allQueriesChecked } = this.state;
|
||||
const { renderHelpText, handleCheckAll, handleCheckQuery } = this;
|
||||
@ -103,7 +105,6 @@ class QueriesList extends Component {
|
||||
const wrapperClassName = classnames(`${baseClass}__table`, {
|
||||
[`${baseClass}__table--query-selected`]: size(checkedQueryIDs),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<table className={wrapperClassName}>
|
||||
@ -118,6 +119,11 @@ class QueriesList extends Component {
|
||||
</th>
|
||||
<th>Query name</th>
|
||||
<th>Description</th>
|
||||
{isOnlyObserver ? null : (
|
||||
<th className={`${baseClass}__observers-can-run`}>
|
||||
Observers can run
|
||||
</th>
|
||||
)}
|
||||
<th className={`${baseClass}__author-name`}>Author</th>
|
||||
<th>Last modified</th>
|
||||
</tr>
|
||||
@ -134,6 +140,7 @@ class QueriesList extends Component {
|
||||
onSelect={onSelectQuery}
|
||||
onDoubleClick={onDblClickQuery}
|
||||
query={query}
|
||||
isOnlyObserver={isOnlyObserver}
|
||||
selected={
|
||||
allQueriesChecked || selectedQuery.id === query.id
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ class QueriesListRow extends Component {
|
||||
onDoubleClick: PropTypes.func,
|
||||
query: queryInterface.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
isOnlyObserver: PropTypes.bool,
|
||||
};
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
@ -47,7 +48,7 @@ class QueriesListRow extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { checked, query, selected } = this.props;
|
||||
const { checked, query, selected, isOnlyObserver } = this.props;
|
||||
const { onCheck, onSelect, onDblClick } = this;
|
||||
const {
|
||||
author_name: authorName,
|
||||
@ -55,6 +56,7 @@ class QueriesListRow extends Component {
|
||||
name,
|
||||
updated_at: updatedAt,
|
||||
description,
|
||||
observer_can_run,
|
||||
} = query;
|
||||
const lastModifiedDate = moment(updatedAt).format("MM/DD/YY");
|
||||
const rowClassName = classnames(baseClass, {
|
||||
@ -76,6 +78,11 @@ class QueriesListRow extends Component {
|
||||
</td>
|
||||
<td className={`${baseClass}__name`}>{name}</td>
|
||||
<td className={`${baseClass}__description`}>{description}</td>
|
||||
{isOnlyObserver ? null : (
|
||||
<td className={`${baseClass}__observers-can-run`}>
|
||||
{observer_can_run.toString()}
|
||||
</td>
|
||||
)}
|
||||
<td className={`${baseClass}__author-name`}>{authorName}</td>
|
||||
<td>{lastModifiedDate}</td>
|
||||
</ClickableTableRow>
|
||||
|
@ -6,6 +6,10 @@
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
&__observers-can-run {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
@ -2,9 +2,11 @@ import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import permissionUtils from "utilities/permissions";
|
||||
import Button from "components/buttons/Button";
|
||||
import KolideAce from "components/KolideAce";
|
||||
import queryInterface from "interfaces/query";
|
||||
import userInterface from "interfaces/user";
|
||||
import SecondarySidePanelContainer from "components/side_panels/SecondarySidePanelContainer";
|
||||
|
||||
const baseClass = "query-details-side-panel";
|
||||
@ -13,6 +15,7 @@ class QueryDetailsSidePanel extends Component {
|
||||
static propTypes = {
|
||||
onEditQuery: PropTypes.func.isRequired,
|
||||
query: queryInterface.isRequired,
|
||||
currentUser: userInterface,
|
||||
};
|
||||
|
||||
handleEditQueryClick = (evt) => {
|
||||
@ -57,9 +60,27 @@ class QueryDetailsSidePanel extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { query } = this.props;
|
||||
const { query, currentUser } = this.props;
|
||||
const { handleEditQueryClick, renderPacks } = this;
|
||||
const { description, name, query: queryText } = query;
|
||||
const { description, name, query: queryText, observer_can_run } = query;
|
||||
|
||||
const renderCTA = () => {
|
||||
if (
|
||||
permissionUtils.isGlobalAdmin(currentUser) ||
|
||||
permissionUtils.isGlobalMaintainer(currentUser)
|
||||
) {
|
||||
return "Edit or run query";
|
||||
}
|
||||
if (
|
||||
permissionUtils.isAnyTeamMaintainer(currentUser) ||
|
||||
(permissionUtils.isOnlyObserver(currentUser) && observer_can_run)
|
||||
) {
|
||||
return "Run query";
|
||||
}
|
||||
if (permissionUtils.isOnlyObserver(currentUser) && !observer_can_run) {
|
||||
return "Show query";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SecondarySidePanelContainer className={baseClass}>
|
||||
@ -81,8 +102,8 @@ class QueryDetailsSidePanel extends Component {
|
||||
</p>
|
||||
<p className={`${baseClass}__label`}>Packs</p>
|
||||
{renderPacks()}
|
||||
<Button onClick={handleEditQueryClick} variant="inverse">
|
||||
Edit or run query
|
||||
<Button onClick={handleEditQueryClick} variant="brand">
|
||||
{renderCTA(currentUser)}
|
||||
</Button>
|
||||
</SecondarySidePanelContainer>
|
||||
);
|
||||
|
@ -3,12 +3,16 @@ import { mount } from "enzyme";
|
||||
import { noop } from "lodash";
|
||||
|
||||
import QueryDetailsSidePanel from "components/side_panels/QueryDetailsSidePanel";
|
||||
import { queryStub } from "test/stubs";
|
||||
import { queryStub, userStub } from "test/stubs";
|
||||
|
||||
describe("QueryDetailsSidePanel - component", () => {
|
||||
it("renders", () => {
|
||||
const component = mount(
|
||||
<QueryDetailsSidePanel onEditQuery={noop} query={queryStub} />
|
||||
<QueryDetailsSidePanel
|
||||
onEditQuery={noop}
|
||||
query={queryStub}
|
||||
currentUser={userStub}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(component.length).toEqual(1);
|
||||
@ -16,7 +20,11 @@ describe("QueryDetailsSidePanel - component", () => {
|
||||
|
||||
it("renders a read-only Kolide Ace component with the query text", () => {
|
||||
const component = mount(
|
||||
<QueryDetailsSidePanel onEditQuery={noop} query={queryStub} />
|
||||
<QueryDetailsSidePanel
|
||||
onEditQuery={noop}
|
||||
query={queryStub}
|
||||
currentUser={userStub}
|
||||
/>
|
||||
);
|
||||
const aceEditor = component.find("KolideAce");
|
||||
|
||||
@ -28,7 +36,11 @@ describe("QueryDetailsSidePanel - component", () => {
|
||||
it("calls the onEditQuery prop when Edit/Run Query is clicked", () => {
|
||||
const spy = jest.fn();
|
||||
const component = mount(
|
||||
<QueryDetailsSidePanel onEditQuery={spy} query={queryStub} />
|
||||
<QueryDetailsSidePanel
|
||||
onEditQuery={spy}
|
||||
query={queryStub}
|
||||
currentUser={userStub}
|
||||
/>
|
||||
);
|
||||
const button = component.find("Button");
|
||||
|
||||
|
@ -7,6 +7,7 @@ export default PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
interval: PropTypes.number,
|
||||
last_excuted: PropTypes.string,
|
||||
observer_can_run: PropTypes.bool,
|
||||
});
|
||||
|
||||
export interface IQuery {
|
||||
@ -16,4 +17,5 @@ export interface IQuery {
|
||||
id: number;
|
||||
interval: number;
|
||||
last_excuted: string;
|
||||
observer_can_run: boolean;
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { filter, get, includes, pull } from "lodash";
|
||||
import { push } from "react-router-redux";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import permissionUtils from "utilities/permissions";
|
||||
import entityGetter from "redux/utilities/entityGetter";
|
||||
import InputField from "components/forms/fields/InputField";
|
||||
import Modal from "components/modals/Modal";
|
||||
@ -15,6 +16,7 @@ import QueryDetailsSidePanel from "components/side_panels/QueryDetailsSidePanel"
|
||||
import QueriesList from "components/queries/QueriesList";
|
||||
import queryActions from "redux/nodes/entities/queries/actions";
|
||||
import queryInterface from "interfaces/query";
|
||||
import userInterface from "interfaces/user";
|
||||
import { renderFlash } from "redux/nodes/notifications/actions";
|
||||
|
||||
const baseClass = "manage-queries-page";
|
||||
@ -25,6 +27,7 @@ export class ManageQueriesPage extends Component {
|
||||
loadingQueries: PropTypes.bool.isRequired,
|
||||
queries: PropTypes.arrayOf(queryInterface),
|
||||
selectedQuery: queryInterface,
|
||||
currentUser: userInterface,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -183,6 +186,8 @@ export class ManageQueriesPage extends Component {
|
||||
|
||||
renderCTAs = () => {
|
||||
const { goToNewQueryPage, onToggleModal } = this;
|
||||
const { currentUser } = this.props;
|
||||
|
||||
const btnClass = `${baseClass}__delete-queries-btn`;
|
||||
const checkedQueryCount = this.state.checkedQueryIDs.length;
|
||||
|
||||
@ -201,11 +206,14 @@ export class ManageQueriesPage extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="brand" onClick={goToNewQueryPage}>
|
||||
Create new query
|
||||
</Button>
|
||||
);
|
||||
// Render option to create new query only for maintainers and admin
|
||||
if (!permissionUtils.isOnlyObserver(currentUser)) {
|
||||
return (
|
||||
<Button variant="brand" onClick={goToNewQueryPage}>
|
||||
Create new query
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
renderModal = () => {
|
||||
@ -233,7 +241,7 @@ export class ManageQueriesPage extends Component {
|
||||
|
||||
renderSidePanel = () => {
|
||||
const { goToEditQueryPage } = this;
|
||||
const { selectedQuery } = this.props;
|
||||
const { selectedQuery, currentUser } = this.props;
|
||||
|
||||
if (!selectedQuery) {
|
||||
// FIXME: Render QueryDetailsSidePanel when Fritz has completed the mock
|
||||
@ -251,6 +259,7 @@ export class ManageQueriesPage extends Component {
|
||||
<QueryDetailsSidePanel
|
||||
onEditQuery={goToEditQueryPage}
|
||||
query={selectedQuery}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -268,12 +277,18 @@ export class ManageQueriesPage extends Component {
|
||||
renderModal,
|
||||
renderSidePanel,
|
||||
} = this;
|
||||
const { loadingQueries, queries: allQueries, selectedQuery } = this.props;
|
||||
const {
|
||||
loadingQueries,
|
||||
queries: allQueries,
|
||||
selectedQuery,
|
||||
currentUser,
|
||||
} = this.props;
|
||||
const queries = getQueries();
|
||||
const queriesCount = queries.length;
|
||||
const queriesTotalDisplay =
|
||||
queriesCount === 1 ? "1 query" : `${queriesCount} queries`;
|
||||
const isQueriesAvailable = allQueries.length > 0;
|
||||
const isOnlyObserver = permissionUtils.isOnlyObserver(currentUser);
|
||||
|
||||
if (loadingQueries) {
|
||||
return false;
|
||||
@ -307,6 +322,7 @@ export class ManageQueriesPage extends Component {
|
||||
onDblClickQuery={onDblClickQuery}
|
||||
queries={queries}
|
||||
selectedQuery={selectedQuery}
|
||||
isOnlyObserver={isOnlyObserver}
|
||||
/>
|
||||
</div>
|
||||
{renderSidePanel()}
|
||||
@ -323,8 +339,9 @@ const mapStateToProps = (state, { location }) => {
|
||||
const selectedQuery =
|
||||
selectedQueryID && queryEntities.findBy({ id: selectedQueryID });
|
||||
const { loading: loadingQueries } = state.entities.queries;
|
||||
const currentUser = state.auth.user;
|
||||
|
||||
return { loadingQueries, queries, selectedQuery };
|
||||
return { loadingQueries, queries, selectedQuery, currentUser };
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(ManageQueriesPage);
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
reduxMockStore,
|
||||
} from "test/helpers";
|
||||
import queryActions from "redux/nodes/entities/queries/actions";
|
||||
import { queryStub } from "test/stubs";
|
||||
import { queryStub, userStub } from "test/stubs";
|
||||
|
||||
const store = {
|
||||
entities: {
|
||||
@ -27,6 +27,9 @@ const store = {
|
||||
},
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
user: userStub,
|
||||
},
|
||||
};
|
||||
|
||||
describe("ManageQueriesPage - component", () => {
|
||||
@ -40,6 +43,7 @@ describe("ManageQueriesPage - component", () => {
|
||||
it("does not render if queries are loading", () => {
|
||||
const loadingQueriesStore = {
|
||||
entities: { queries: { loading: true, data: {} } },
|
||||
auth: { user: userStub },
|
||||
};
|
||||
const Component = connectedComponent(ConnectedManageQueriesPage, {
|
||||
mockStore: reduxMockStore(loadingQueriesStore),
|
||||
@ -50,6 +54,7 @@ describe("ManageQueriesPage - component", () => {
|
||||
});
|
||||
|
||||
it("renders a QueriesList component", () => {
|
||||
console.log(store);
|
||||
const Component = connectedComponent(ConnectedManageQueriesPage, {
|
||||
mockStore: reduxMockStore(store),
|
||||
});
|
||||
@ -76,6 +81,7 @@ describe("ManageQueriesPage - component", () => {
|
||||
dispatch: () => Promise.resolve(),
|
||||
loadingQueries: false,
|
||||
queries: [queryStub],
|
||||
currentUser: userStub,
|
||||
};
|
||||
const page = mount(<ManageQueriesPage {...props} />);
|
||||
|
||||
@ -97,6 +103,7 @@ describe("ManageQueriesPage - component", () => {
|
||||
dispatch: () => Promise.reject(),
|
||||
loadingQueries: false,
|
||||
queries: [queryStub],
|
||||
currentUser: userStub,
|
||||
};
|
||||
const page = mount(<ManageQueriesPage {...props} />);
|
||||
|
||||
@ -128,7 +135,9 @@ describe("ManageQueriesPage - component", () => {
|
||||
});
|
||||
|
||||
it("updates checkedQueryIDs in state when the check all queries Checkbox is toggled", () => {
|
||||
const page = mount(<ManageQueriesPage queries={[queryStub]} />);
|
||||
const page = mount(
|
||||
<ManageQueriesPage queries={[queryStub]} currentUser={userStub} />
|
||||
);
|
||||
const selectAllQueries = page.find({ name: "check-all-queries" });
|
||||
|
||||
expect(page.state("checkedQueryIDs")).toEqual([]);
|
||||
@ -143,7 +152,9 @@ describe("ManageQueriesPage - component", () => {
|
||||
});
|
||||
|
||||
it("updates checkedQueryIDs in state when a query row Checkbox is toggled", () => {
|
||||
const page = mount(<ManageQueriesPage queries={[queryStub]} />);
|
||||
const page = mount(
|
||||
<ManageQueriesPage queries={[queryStub]} currentUser={userStub} />
|
||||
);
|
||||
const queryCheckbox = page.find({ name: `query-checkbox-${queryStub.id}` });
|
||||
|
||||
expect(page.state("checkedQueryIDs")).toEqual([]);
|
||||
@ -207,7 +218,9 @@ describe("ManageQueriesPage - component", () => {
|
||||
const queries = [queryStub, { ...queryStub, id: 101, name: "alpha query" }];
|
||||
|
||||
it("displays the delete action button when a query is checked", () => {
|
||||
const page = mount(<ManageQueriesPage queries={queries} />);
|
||||
const page = mount(
|
||||
<ManageQueriesPage queries={queries} currentUser={userStub} />
|
||||
);
|
||||
const checkAllQueries = page.find({ name: "check-all-queries" });
|
||||
|
||||
checkAllQueries.hostNodes().simulate("change");
|
||||
|
@ -137,6 +137,7 @@ export const queryStub = {
|
||||
snapshot: false,
|
||||
updated_at: "2016-10-17T07:06:00Z",
|
||||
version: "",
|
||||
observer_can_run: true,
|
||||
};
|
||||
|
||||
export const scheduledQueryStub = {
|
||||
|
Loading…
Reference in New Issue
Block a user