mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
feat: add audit page title, subtitle, and CLI snippet (#3419)
* resolves #3356 * scaffolded out new audit page header resolves #3357 * added tests and stories * run prettier
This commit is contained in:
@ -8,18 +8,28 @@ export interface CodeExampleProps {
|
||||
code: string
|
||||
className?: string
|
||||
buttonClassName?: string
|
||||
tooltipTitle?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to show single-line code examples, with a copy button
|
||||
*/
|
||||
export const CodeExample: FC<CodeExampleProps> = ({ code, className, buttonClassName }) => {
|
||||
export const CodeExample: FC<CodeExampleProps> = ({
|
||||
code,
|
||||
className,
|
||||
buttonClassName,
|
||||
tooltipTitle,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<div className={combineClasses([styles.root, className])}>
|
||||
<code className={styles.code}>{code}</code>
|
||||
<CopyButton text={code} buttonClassName={combineClasses([styles.button, buttonClassName])} />
|
||||
<CopyButton
|
||||
text={code}
|
||||
tooltipTitle={tooltipTitle}
|
||||
buttonClassName={combineClasses([styles.button, buttonClassName])}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -11,6 +11,12 @@ interface CopyButtonProps {
|
||||
ctaCopy?: string
|
||||
wrapperClassName?: string
|
||||
buttonClassName?: string
|
||||
tooltipTitle?: string
|
||||
}
|
||||
|
||||
export const Language = {
|
||||
tooltipTitle: "Copy to clipboard",
|
||||
ariaLabel: "Copy to clipboard",
|
||||
}
|
||||
|
||||
/**
|
||||
@ -21,6 +27,7 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
|
||||
ctaCopy,
|
||||
wrapperClassName = "",
|
||||
buttonClassName = "",
|
||||
tooltipTitle = Language.tooltipTitle,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
||||
@ -56,12 +63,13 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title="Copy to Clipboard" placement="top">
|
||||
<Tooltip title={tooltipTitle} placement="top">
|
||||
<div className={combineClasses([styles.copyButtonWrapper, wrapperClassName])}>
|
||||
<IconButton
|
||||
className={combineClasses([styles.copyButton, buttonClassName])}
|
||||
onClick={copyToClipboard}
|
||||
size="small"
|
||||
aria-label={Language.ariaLabel}
|
||||
>
|
||||
{isCopied ? (
|
||||
<Check className={styles.fileCopyIcon} />
|
||||
|
@ -5,18 +5,23 @@ import { combineClasses } from "../../util/combineClasses"
|
||||
|
||||
type Direction = "column" | "row"
|
||||
|
||||
interface StyleProps {
|
||||
direction: Direction
|
||||
spacing: number
|
||||
export interface StackProps {
|
||||
className?: string
|
||||
direction?: Direction
|
||||
spacing?: number
|
||||
alignItems?: CSSProperties["alignItems"]
|
||||
justifyContent?: CSSProperties["justifyContent"]
|
||||
}
|
||||
|
||||
type StyleProps = Omit<StackProps, "className">
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
stack: {
|
||||
display: "flex",
|
||||
flexDirection: ({ direction }: StyleProps) => direction,
|
||||
gap: ({ spacing }: StyleProps) => theme.spacing(spacing),
|
||||
gap: ({ spacing }: StyleProps) => spacing && theme.spacing(spacing),
|
||||
alignItems: ({ alignItems }: StyleProps) => alignItems,
|
||||
justifyContent: ({ justifyContent }: StyleProps) => justifyContent,
|
||||
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
width: "100%",
|
||||
@ -24,21 +29,15 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
}))
|
||||
|
||||
export interface StackProps {
|
||||
className?: string
|
||||
direction?: Direction
|
||||
spacing?: number
|
||||
alignItems?: CSSProperties["alignItems"]
|
||||
}
|
||||
|
||||
export const Stack: FC<StackProps> = ({
|
||||
children,
|
||||
className,
|
||||
direction = "column",
|
||||
spacing = 2,
|
||||
alignItems,
|
||||
justifyContent,
|
||||
}) => {
|
||||
const styles = useStyles({ spacing, direction, alignItems })
|
||||
const styles = useStyles({ spacing, direction, alignItems, justifyContent })
|
||||
|
||||
return <div className={combineClasses([styles.stack, className])}>{children}</div>
|
||||
}
|
||||
|
16
site/src/components/Tooltips/AuditHelpTooltip.tsx
Normal file
16
site/src/components/Tooltips/AuditHelpTooltip.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { FC } from "react"
|
||||
import { HelpTooltip, HelpTooltipText, HelpTooltipTitle } from "./HelpTooltip"
|
||||
|
||||
export const Language = {
|
||||
title: "What is an audit log?",
|
||||
body: "An audit log is a record of events and changes made throughout a system.",
|
||||
}
|
||||
|
||||
export const AuditHelpTooltip: FC = () => {
|
||||
return (
|
||||
<HelpTooltip>
|
||||
<HelpTooltipTitle>{Language.title}</HelpTooltipTitle>
|
||||
<HelpTooltipText>{Language.body}</HelpTooltipText>
|
||||
</HelpTooltip>
|
||||
)
|
||||
}
|
@ -15,6 +15,10 @@ export interface HelpTooltipProps {
|
||||
size?: Size
|
||||
}
|
||||
|
||||
export const Language = {
|
||||
ariaLabel: "tooltip",
|
||||
}
|
||||
|
||||
const HelpTooltipContext = createContext<{ open: boolean; onClose: () => void } | undefined>(
|
||||
undefined,
|
||||
)
|
||||
@ -52,6 +56,7 @@ export const HelpTooltip: React.FC<HelpTooltipProps> = ({ children, open, size =
|
||||
onMouseEnter={() => {
|
||||
setIsOpen(true)
|
||||
}}
|
||||
aria-label={Language.ariaLabel}
|
||||
>
|
||||
<HelpIcon className={styles.icon} />
|
||||
</button>
|
||||
|
@ -1,4 +1,5 @@
|
||||
export { AgentHelpTooltip } from "./AgentHelpTooltip"
|
||||
export { AuditHelpTooltip } from "./AuditHelpTooltip"
|
||||
export { OutdatedHelpTooltip } from "./OutdatedHelpTooltip"
|
||||
export { ResourcesHelpTooltip } from "./ResourcesHelpTooltip"
|
||||
export { WorkspaceHelpTooltip } from "./WorkspaceHelpTooltip"
|
||||
|
32
site/src/pages/AuditPage/AuditPage.test.tsx
Normal file
32
site/src/pages/AuditPage/AuditPage.test.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { fireEvent, screen } from "@testing-library/react"
|
||||
import { Language as CopyButtonLanguage } from "components/CopyButton/CopyButton"
|
||||
import { Language as AuditTooltipLanguage } from "components/Tooltips/AuditHelpTooltip"
|
||||
import { Language as TooltipLanguage } from "components/Tooltips/HelpTooltip/HelpTooltip"
|
||||
import { render } from "testHelpers/renderHelpers"
|
||||
import AuditPage from "./AuditPage"
|
||||
import { Language as AuditViewLanguage } from "./AuditPageView"
|
||||
|
||||
describe("AuditPage", () => {
|
||||
it("renders a page with a title and subtitle", async () => {
|
||||
// When
|
||||
render(<AuditPage />)
|
||||
|
||||
// Then
|
||||
await screen.findByText(AuditViewLanguage.title)
|
||||
await screen.findByText(AuditViewLanguage.subtitle)
|
||||
const tooltipIcon = await screen.findByRole("button", { name: TooltipLanguage.ariaLabel })
|
||||
fireEvent.mouseOver(tooltipIcon)
|
||||
expect(await screen.findByText(AuditTooltipLanguage.title)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("describes the CLI command", async () => {
|
||||
// When
|
||||
render(<AuditPage />)
|
||||
|
||||
// Then
|
||||
await screen.findByText("coder audit [organization_ID]") // CLI command; untranslated
|
||||
const copyIcon = await screen.findByRole("button", { name: CopyButtonLanguage.ariaLabel })
|
||||
fireEvent.mouseOver(copyIcon)
|
||||
expect(await screen.findByText(AuditViewLanguage.tooltipTitle)).toBeInTheDocument()
|
||||
})
|
||||
})
|
@ -1,8 +1,9 @@
|
||||
import { FC } from "react"
|
||||
import { AuditPageView } from "./AuditPageView"
|
||||
|
||||
// REMARK: This page is in-progress and hidden from users
|
||||
const AuditPage: FC = () => {
|
||||
return <div>Audit</div>
|
||||
return <AuditPageView />
|
||||
}
|
||||
|
||||
export default AuditPage
|
||||
|
16
site/src/pages/AuditPage/AuditPageView.stories.tsx
Normal file
16
site/src/pages/AuditPage/AuditPageView.stories.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import { AuditPageView } from "./AuditPageView"
|
||||
|
||||
export default {
|
||||
title: "pages/AuditPageView",
|
||||
component: AuditPageView,
|
||||
} as ComponentMeta<typeof AuditPageView>
|
||||
|
||||
const Template: Story = (args) => <AuditPageView {...args} />
|
||||
|
||||
export const AuditPage = Template.bind({})
|
||||
|
||||
export const AuditPageSmallViewport = Template.bind({})
|
||||
AuditPageSmallViewport.parameters = {
|
||||
chromatic: { viewports: [600] },
|
||||
}
|
59
site/src/pages/AuditPage/AuditPageView.tsx
Normal file
59
site/src/pages/AuditPage/AuditPageView.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { CodeExample } from "components/CodeExample/CodeExample"
|
||||
import { Margins } from "components/Margins/Margins"
|
||||
import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "components/PageHeader/PageHeader"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { AuditHelpTooltip } from "components/Tooltips"
|
||||
import { FC } from "react"
|
||||
|
||||
export const Language = {
|
||||
title: "Audit",
|
||||
subtitle: "View events in your audit log.",
|
||||
tooltipTitle: "Copy to clipboard and try the Coder CLI",
|
||||
}
|
||||
|
||||
export const AuditPageView: FC = () => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
<Stack justifyContent="space-between" className={styles.headingContainer}>
|
||||
<PageHeader className={styles.headingStyles}>
|
||||
<PageHeaderTitle>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<span>{Language.title}</span>
|
||||
<AuditHelpTooltip />
|
||||
</Stack>
|
||||
</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>{Language.subtitle}</PageHeaderSubtitle>
|
||||
</PageHeader>
|
||||
<CodeExample
|
||||
className={styles.codeExampleStyles}
|
||||
tooltipTitle={Language.tooltipTitle}
|
||||
code="coder audit [organization_ID]"
|
||||
/>
|
||||
</Stack>
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
headingContainer: {
|
||||
marginTop: theme.spacing(6),
|
||||
marginBottom: theme.spacing(5),
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
flexDirection: "column",
|
||||
alignItems: "start",
|
||||
},
|
||||
},
|
||||
headingStyles: {
|
||||
paddingTop: "0px",
|
||||
paddingBottom: "0px",
|
||||
},
|
||||
codeExampleStyles: {
|
||||
height: "fit-content",
|
||||
},
|
||||
}))
|
Reference in New Issue
Block a user