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
|
code: string
|
||||||
className?: string
|
className?: string
|
||||||
buttonClassName?: string
|
buttonClassName?: string
|
||||||
|
tooltipTitle?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component to show single-line code examples, with a copy button
|
* 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()
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={combineClasses([styles.root, className])}>
|
<div className={combineClasses([styles.root, className])}>
|
||||||
<code className={styles.code}>{code}</code>
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,12 @@ interface CopyButtonProps {
|
|||||||
ctaCopy?: string
|
ctaCopy?: string
|
||||||
wrapperClassName?: string
|
wrapperClassName?: string
|
||||||
buttonClassName?: 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,
|
ctaCopy,
|
||||||
wrapperClassName = "",
|
wrapperClassName = "",
|
||||||
buttonClassName = "",
|
buttonClassName = "",
|
||||||
|
tooltipTitle = Language.tooltipTitle,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const [isCopied, setIsCopied] = useState<boolean>(false)
|
const [isCopied, setIsCopied] = useState<boolean>(false)
|
||||||
@ -56,12 +63,13 @@ export const CopyButton: React.FC<CopyButtonProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title="Copy to Clipboard" placement="top">
|
<Tooltip title={tooltipTitle} placement="top">
|
||||||
<div className={combineClasses([styles.copyButtonWrapper, wrapperClassName])}>
|
<div className={combineClasses([styles.copyButtonWrapper, wrapperClassName])}>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={combineClasses([styles.copyButton, buttonClassName])}
|
className={combineClasses([styles.copyButton, buttonClassName])}
|
||||||
onClick={copyToClipboard}
|
onClick={copyToClipboard}
|
||||||
size="small"
|
size="small"
|
||||||
|
aria-label={Language.ariaLabel}
|
||||||
>
|
>
|
||||||
{isCopied ? (
|
{isCopied ? (
|
||||||
<Check className={styles.fileCopyIcon} />
|
<Check className={styles.fileCopyIcon} />
|
||||||
|
@ -5,18 +5,23 @@ import { combineClasses } from "../../util/combineClasses"
|
|||||||
|
|
||||||
type Direction = "column" | "row"
|
type Direction = "column" | "row"
|
||||||
|
|
||||||
interface StyleProps {
|
export interface StackProps {
|
||||||
direction: Direction
|
className?: string
|
||||||
spacing: number
|
direction?: Direction
|
||||||
|
spacing?: number
|
||||||
alignItems?: CSSProperties["alignItems"]
|
alignItems?: CSSProperties["alignItems"]
|
||||||
|
justifyContent?: CSSProperties["justifyContent"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StyleProps = Omit<StackProps, "className">
|
||||||
|
|
||||||
const useStyles = makeStyles((theme) => ({
|
const useStyles = makeStyles((theme) => ({
|
||||||
stack: {
|
stack: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: ({ direction }: StyleProps) => direction,
|
flexDirection: ({ direction }: StyleProps) => direction,
|
||||||
gap: ({ spacing }: StyleProps) => theme.spacing(spacing),
|
gap: ({ spacing }: StyleProps) => spacing && theme.spacing(spacing),
|
||||||
alignItems: ({ alignItems }: StyleProps) => alignItems,
|
alignItems: ({ alignItems }: StyleProps) => alignItems,
|
||||||
|
justifyContent: ({ justifyContent }: StyleProps) => justifyContent,
|
||||||
|
|
||||||
[theme.breakpoints.down("sm")]: {
|
[theme.breakpoints.down("sm")]: {
|
||||||
width: "100%",
|
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> = ({
|
export const Stack: FC<StackProps> = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
direction = "column",
|
direction = "column",
|
||||||
spacing = 2,
|
spacing = 2,
|
||||||
alignItems,
|
alignItems,
|
||||||
|
justifyContent,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles({ spacing, direction, alignItems })
|
const styles = useStyles({ spacing, direction, alignItems, justifyContent })
|
||||||
|
|
||||||
return <div className={combineClasses([styles.stack, className])}>{children}</div>
|
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
|
size?: Size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const Language = {
|
||||||
|
ariaLabel: "tooltip",
|
||||||
|
}
|
||||||
|
|
||||||
const HelpTooltipContext = createContext<{ open: boolean; onClose: () => void } | undefined>(
|
const HelpTooltipContext = createContext<{ open: boolean; onClose: () => void } | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
)
|
)
|
||||||
@ -52,6 +56,7 @@ export const HelpTooltip: React.FC<HelpTooltipProps> = ({ children, open, size =
|
|||||||
onMouseEnter={() => {
|
onMouseEnter={() => {
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
}}
|
}}
|
||||||
|
aria-label={Language.ariaLabel}
|
||||||
>
|
>
|
||||||
<HelpIcon className={styles.icon} />
|
<HelpIcon className={styles.icon} />
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export { AgentHelpTooltip } from "./AgentHelpTooltip"
|
export { AgentHelpTooltip } from "./AgentHelpTooltip"
|
||||||
|
export { AuditHelpTooltip } from "./AuditHelpTooltip"
|
||||||
export { OutdatedHelpTooltip } from "./OutdatedHelpTooltip"
|
export { OutdatedHelpTooltip } from "./OutdatedHelpTooltip"
|
||||||
export { ResourcesHelpTooltip } from "./ResourcesHelpTooltip"
|
export { ResourcesHelpTooltip } from "./ResourcesHelpTooltip"
|
||||||
export { WorkspaceHelpTooltip } from "./WorkspaceHelpTooltip"
|
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 { FC } from "react"
|
||||||
|
import { AuditPageView } from "./AuditPageView"
|
||||||
|
|
||||||
// REMARK: This page is in-progress and hidden from users
|
// REMARK: This page is in-progress and hidden from users
|
||||||
const AuditPage: FC = () => {
|
const AuditPage: FC = () => {
|
||||||
return <div>Audit</div>
|
return <AuditPageView />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AuditPage
|
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