Skip to content

Commit

Permalink
Adds single state UI component (#53)
Browse files Browse the repository at this point in the history
Signed-off-by: Drew Baugher <[email protected]>
  • Loading branch information
dbbaughe authored Aug 11, 2021
1 parent c70a926 commit a25e3fe
Show file tree
Hide file tree
Showing 4 changed files with 480 additions and 2 deletions.
4 changes: 2 additions & 2 deletions models/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ export interface ISMTemplate {

export interface State {
name: string;
actions: object[];
transitions: object[];
actions?: Action[];
transitions?: Transition[];
}

export interface Action {
Expand Down
86 changes: 86 additions & 0 deletions public/pages/VisualCreatePolicy/components/States/State.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

import React from "react";
import "@testing-library/jest-dom/extend-expect";
import { screen, render } from "@testing-library/react";
import State from "./State";
import { State as StateData } from "../../../../../models/interfaces";
import { ModalProvider, ModalRoot } from "../../../../components/Modal";
import { ServicesConsumer, ServicesContext } from "../../../../services";
import { browserServicesMock } from "../../../../../test/mocks";
import { fireEvent, waitFor } from "@testing-library/dom";

describe("<State /> spec", () => {
it("renders the component", () => {
const state: StateData = {
name: "some_name",
actions: [{ close: {} }, { open: {} }, { delete: {} }],
transitions: [{ state_name: "elsewhere", conditions: {} }],
};
const { container } = render(
<State state={state} isInitialState={true} idx={2} onClickEditState={() => {}} onClickDeleteState={() => {}} />
);
expect(container.firstChild).toMatchSnapshot();
});

it("does not show initial state when it is not the initial state", async () => {
const state: StateData = {
name: "some_name",
actions: [{ close: {} }, { open: {} }, { delete: {} }],
transitions: [{ state_name: "elsewhere", conditions: {} }],
};
render(<State state={state} isInitialState={false} idx={2} onClickEditState={() => {}} onClickDeleteState={() => {}} />);

await waitFor(() => screen.getByText("some_name", { exact: false }));

expect(screen.queryByText("Initial state", { exact: false })).toBeNull();
});

it("shows no transitions message and no actions message if none defined", async () => {
const state: StateData = {
name: "some_name",
actions: [],
transitions: [],
};
render(<State state={state} isInitialState={false} idx={2} onClickEditState={() => {}} onClickDeleteState={() => {}} />);

await waitFor(() => screen.getByText("some_name", { exact: false }));

expect(screen.queryByText("No transitions. Edit state to add transitions.", { exact: false })).not.toBeNull();
expect(screen.queryByText("No actions. Edit state to add actions.", { exact: false })).not.toBeNull();
});

it("renders delete modal when deleting a state", async () => {
const state: StateData = {
name: "some_name",
actions: [{ close: {} }, { open: {} }, { delete: {} }],
transitions: [{ state_name: "elsewhere", conditions: {} }],
};
const onClickDeleteState = jest.fn();
const { getByTestId } = render(
<ServicesContext.Provider value={browserServicesMock}>
<ModalProvider>
<ServicesConsumer>{(services) => services && <ModalRoot services={services} />}</ServicesConsumer>
<State state={state} isInitialState={true} idx={2} onClickEditState={() => {}} onClickDeleteState={onClickDeleteState} />
</ModalProvider>
</ServicesContext.Provider>
);

fireEvent.click(getByTestId("state-delete-button"));

await waitFor(() => screen.getByText("Deleting the state will result in deleting all transitions.", { exact: false }));

fireEvent.click(getByTestId("confirmationModalActionButton"));

expect(onClickDeleteState).toHaveBeenCalled();
});
});
145 changes: 145 additions & 0 deletions public/pages/VisualCreatePolicy/components/States/State.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/

import React from "react";
import { EuiAccordion, EuiText, EuiPanel, EuiFlexGroup, EuiFlexItem, EuiButtonIcon } from "@elastic/eui";
import "brace/theme/github";
import "brace/mode/json";
import { State as StateData } from "../../../../../models/interfaces";
import { ModalConsumer } from "../../../../components/Modal";
import ConfirmationModal from "../../../../components/ConfirmationModal";
import Badge from "../Badge";
import TransitionContent from "../Transition/TransitionContent";
import { makeId } from "../../../../utils/helpers";
import { getUIActionFromData } from "../../utils/helpers";

interface StateProps {
state: StateData;
isInitialState: boolean;
idx: number;
onClickEditState: (state: StateData) => void;
onClickDeleteState: (idx: number) => void;
}

const State = ({ state, isInitialState, idx, onClickEditState, onClickDeleteState }: StateProps) => (
<EuiAccordion
style={{ padding: "15px" }}
id={state.name}
buttonContent={
<EuiFlexGroup justifyContent="center" alignItems="center">
<EuiFlexItem grow={false}>
<EuiText>
<strong>{state.name}</strong>
</EuiText>
</EuiFlexItem>
{isInitialState && (
<EuiFlexItem grow={false}>
<EuiPanel paddingSize="none">
<EuiText size="xs" style={{ padding: "0px 10px" }}>
Initial state
</EuiText>
</EuiPanel>
</EuiFlexItem>
)}
{!!state.actions?.length && (
<EuiFlexItem grow={false}>
<Badge text="Actions" number={state.actions.length} />
</EuiFlexItem>
)}
{!!state.transitions?.length && (
<EuiFlexItem grow={false}>
<Badge text="Transitions" number={state.transitions.length} />
</EuiFlexItem>
)}
</EuiFlexGroup>
}
extraAction={
<EuiFlexGroup gutterSize="m">
<EuiFlexItem grow={false}>
<ModalConsumer>
{({ onShow, onClose }) => (
<EuiButtonIcon
iconType="trash"
aria-label="Delete"
color="danger"
onClick={() =>
onShow(ConfirmationModal, {
title: "Delete state",
bodyMessage: (
<EuiText>
<span>
Delete "<strong>{state.name}</strong>" permanently? Deleting the state will result in deleting all transitions.
</span>
</EuiText>
),
actionMessage: "Delete state",
actionProps: { color: "danger" },
modalProps: { maxWidth: 600 },
onAction: () => onClickDeleteState(idx),
onClose,
})
}
data-test-subj="state-delete-button"
/>
)}
</ModalConsumer>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonIcon iconType="pencil" aria-label="Edit" color="primary" onClick={() => onClickEditState(state)} />
</EuiFlexItem>
</EuiFlexGroup>
}
paddingSize="l"
>
<EuiFlexGroup direction="column" style={{ backgroundColor: "#f8f9fc" }}>
<EuiFlexItem grow={false}>
<EuiText>
<h4>Actions</h4>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
{!state.actions?.length ? (
<EuiText>No actions. Edit state to add actions.</EuiText>
) : (
<EuiFlexGroup>
{state.actions.map((action) => (
<EuiFlexItem grow={false} key={makeId()}>
<EuiPanel>{getUIActionFromData(action).content()}</EuiPanel>
</EuiFlexItem>
))}
</EuiFlexGroup>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText>
<h4>Transitions</h4>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
{!state.transitions?.length ? (
<EuiText>No transitions. Edit state to add transitions.</EuiText>
) : (
<EuiFlexGroup>
{state.transitions.map((transition) => (
<EuiFlexItem grow={false} key={makeId()}>
<EuiPanel>
<TransitionContent transition={transition} />
</EuiPanel>
</EuiFlexItem>
))}
</EuiFlexGroup>
)}
</EuiFlexItem>
</EuiFlexGroup>
</EuiAccordion>
);

export default State;
Loading

0 comments on commit a25e3fe

Please sign in to comment.