From 0fe6f52795e7343578047e9b0f7c6d1c662e70da Mon Sep 17 00:00:00 2001 From: G r e y Date: Sat, 19 Mar 2022 03:41:41 +0000 Subject: [PATCH] feat(site): add TargetCell component Summary: This is a direct follow-up to #484 and #500. It is a part of many steps in porting/refactoring the AuditLog from v1. Details: - Port over TargetCell from v1, with refactorings - Add tests and stories Impact: This change does not have any user-facing impact yet because AuditLog is not yet available in the product. This is part of an incremental approach; the FE is still waiting on the BE port. Relations: - This commit relates to #472, but does not finish it - This commit should not be merged until after #484 and #500, because it builds off of them. --- .../AuditLog/TargetCell.stories.tsx | 21 +++ .../components/AuditLog/TargetCell.test.tsx | 131 ++++++++++++++++++ site/src/components/AuditLog/TargetCell.tsx | 55 ++++++++ 3 files changed, 207 insertions(+) create mode 100644 site/src/components/AuditLog/TargetCell.stories.tsx create mode 100644 site/src/components/AuditLog/TargetCell.test.tsx create mode 100644 site/src/components/AuditLog/TargetCell.tsx diff --git a/site/src/components/AuditLog/TargetCell.stories.tsx b/site/src/components/AuditLog/TargetCell.stories.tsx new file mode 100644 index 0000000000000..ec699bab7ee59 --- /dev/null +++ b/site/src/components/AuditLog/TargetCell.stories.tsx @@ -0,0 +1,21 @@ +import { ComponentMeta, Story } from "@storybook/react" +import React from "react" +import { TargetCell, TargetCellProps } from "./TargetCell" + +export default { + title: "AuditLog/Cells/TargetCell", + component: TargetCell, + argTypes: { + onSelect: { + action: "onSelect", + }, + }, +} as ComponentMeta + +const Template: Story = (args) => + +export const Example = Template.bind({}) +Example.args = { + name: "Coder frontend", + type: "project", +} diff --git a/site/src/components/AuditLog/TargetCell.test.tsx b/site/src/components/AuditLog/TargetCell.test.tsx new file mode 100644 index 0000000000000..0fc567f2cb905 --- /dev/null +++ b/site/src/components/AuditLog/TargetCell.test.tsx @@ -0,0 +1,131 @@ +import React from "react" +import { WrapperComponent } from "../../test_helpers" +import { TargetCell, TargetCellProps, LANGUAGE } from "./TargetCell" +import { fireEvent, render, screen } from "@testing-library/react" + +namespace Helpers { + export const Props: TargetCellProps = { + name: "name", + type: "test", + onSelect: jest.fn(), + } + + export const Component: React.FC = (props) => ( + + + + ) +} + +describe("TargetCellProps", () => { + // eslint-disable-next-line @typescript-eslint/no-empty-function + const noop = () => {} + + it.each<[TargetCellProps, TargetCellProps, boolean]>([ + [ + { + name: "test", + type: "test", + onSelect: noop, + }, + { + name: "test", + type: "test", + onSelect: noop, + }, + false, + ], + [ + { + name: "", + type: " test ", + onSelect: noop, + }, + { + name: "", + type: "test", + onSelect: noop, + }, + false, + ], + [ + { + name: "test", + type: "", + onSelect: noop, + }, + { + name: "test", + type: "", + onSelect: noop, + }, + true, + ], + ])(`validate(%p) -> %p throws: %p`, (props, expected, throws) => { + const validate = () => { + return TargetCellProps.validate(props) + } + + if (throws) { + expect(validate).toThrowError() + } else { + expect(validate()).toStrictEqual(expected) + } + }) +}) + +describe("TargetCell", () => { + // onSelect callback + it("calls onSelect when the name is clicked", () => { + // Given + const onSelectMock = jest.fn() + const props: TargetCellProps = { + ...Helpers.Props, + onSelect: onSelectMock, + } + + // When + render() + fireEvent.click(screen.getByText(props.name)) + + // Then + expect(onSelectMock).toHaveBeenCalledTimes(1) + }) + + // target name cases + it("renders a non-empty name", () => { + // Given + const props = Helpers.Props + + // When + render() + + // Then + expect(screen.getByText(props.name)).toBeDefined() + }) + it(`renders ${LANGUAGE.emptyDisplayName} when name is '""'`, () => { + // Given + const props: TargetCellProps = { + ...Helpers.Props, + name: "", + } + + // When + render() + + // Then + expect(screen.getByText(LANGUAGE.emptyDisplayName)).toBeDefined() + }) + + // target type + it("renders target type", () => { + // Given + const props = Helpers.Props + + // When + render() + + // Then + expect(screen.getByText(props.type)).toBeDefined() + }) +}) diff --git a/site/src/components/AuditLog/TargetCell.tsx b/site/src/components/AuditLog/TargetCell.tsx new file mode 100644 index 0000000000000..f462ccc5b1efd --- /dev/null +++ b/site/src/components/AuditLog/TargetCell.tsx @@ -0,0 +1,55 @@ +import Box from "@material-ui/core/Box" +import Link from "@material-ui/core/Link" +import Typography from "@material-ui/core/Typography" +import React from "react" + +export const LANGUAGE = { + emptyDisplayName: "*", +} + +const TargetCellName = (displayName: string, onSelect: () => void): JSX.Element => { + return displayName ? ( + {displayName} + ) : ( + {LANGUAGE.emptyDisplayName} + ) +} + +export interface TargetCellProps { + name: string + type: string + onSelect: () => void +} +export namespace TargetCellProps { + /** + * @throws Error if invalid + */ + export const validate = (props: TargetCellProps): TargetCellProps => { + const sanitizedName = props.name.trim() + const sanitizedType = props.type.trim() + + if (!sanitizedType) { + throw new Error(`invalid type: '${props.type}'`) + } + + return { + name: sanitizedName, + type: sanitizedType, + onSelect: props.onSelect, + } + } +} + +export const TargetCell: React.FC = (props) => { + const { name, type, onSelect } = TargetCellProps.validate(props) + + return ( + + {TargetCellName(name, onSelect)} + + + {type} + + + ) +}