From b8fa378a02116800f39fa95a7c58d907f64d9671 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 30 Aug 2022 20:23:02 +0000 Subject: [PATCH 01/28] Add basic auditXService code --- site/src/api/api.ts | 31 ++++++++++++++++ site/src/testHelpers/entities.ts | 23 ++++++++++-- site/src/testHelpers/handlers.ts | 5 +++ site/src/xServices/audit/auditXService.ts | 43 +++++++++++++++++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 site/src/xServices/audit/auditXService.ts diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 20a5156e9d3f9..ed2c3b43ddee9 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1,8 +1,10 @@ import axios, { AxiosRequestHeaders } from "axios" import dayjs from "dayjs" +import { MockAuditLog } from "testHelpers/entities" import * as Types from "./types" import { WorkspaceBuildTransition } from "./types" import * as TypesGen from "./typesGenerated" +import { User } from "./typesGenerated" const CONTENT_TYPE_JSON: AxiosRequestHeaders = { "Content-Type": "application/json", @@ -383,3 +385,32 @@ export const getEntitlements = async (): Promise => { const response = await axios.get("/api/v2/entitlements") return response.data } + +interface AuditLog { + readonly id: string + readonly request_id: string + readonly time: string + readonly organization_id: string + // Named type "net/netip.Addr" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly ip: any + readonly user_agent: string + readonly resource_type: any + readonly resource_id: string + readonly resource_target: string + readonly action: any + readonly diff: any + readonly status_code: number + // This is likely an enum in an external package ("encoding/json.RawMessage") + readonly additional_fields: any + readonly description: string + readonly user?: User + // This is likely an enum in an external package ("encoding/json.RawMessage") + readonly resource: any +} + +export const getAuditLogs = async (): Promise => { + return [MockAuditLog] + // const response = await axios.get("/api/v2/audit") + // return response.data +} diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index afdf5f4e18670..deb93d6cad2f8 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -637,8 +637,8 @@ export const makeMockApiError = ({ detail, validations, }: { - message?: string - detail?: string + message?: "" + detail?: "" validations?: FieldError[] }) => ({ response: { @@ -684,3 +684,22 @@ export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { }, }, } + +export const MockAuditLog = { + id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", + request_id: "53bded77-7b9d-4e82-8771-991a34d759f9", + time: "2022-05-19T16:45:57.122Z", + organization_id: "fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0", + ip: "127.0.0.1", + user_agent: "browser", + resource_type: "organization", + resource_id: "ef8d1cf4-82de-4fd9-8980-047dad6d06b5", + resource_target: "Bruno's Org", + action: "create", + diff: {}, + status_code: 200, + additional_fields: {}, + description: "Colin Adler updated the organization Bruno's Org", + user: MockUser, + resource: MockOrganization, +} diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index a12b46179a5be..2f88cd80e84c9 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -147,4 +147,9 @@ export const handlers = [ rest.get("/api/v2/entitlements", (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockEntitlements)) }), + + // Audit + rest.get("/api/v2/audit", (req, res, ctx) => { + return res(ctx.status(200), ctx.json(M.MockAuditLog)) + }), ] diff --git a/site/src/xServices/audit/auditXService.ts b/site/src/xServices/audit/auditXService.ts new file mode 100644 index 0000000000000..defc75febd4f5 --- /dev/null +++ b/site/src/xServices/audit/auditXService.ts @@ -0,0 +1,43 @@ +import { getAuditLogs } from "api/api" +import { assign, createMachine } from "xstate" + +type AuditLogs = Awaited> + +export const auditMachine = createMachine( + { + id: "auditMachine", + schema: { + context: {} as { auditLogs: AuditLogs }, + services: {} as { + loadAuditLogs: { + data: AuditLogs + } + }, + }, + tsTypes: {} as import("./auditXService.typegen").Typegen0, + states: { + loadingLogs: { + invoke: { + src: "loadAuditLogs", + onDone: { + target: "success", + actions: ["assignAuditLogs"], + }, + }, + }, + success: { + type: "final", + }, + }, + }, + { + actions: { + assignAuditLogs: assign({ + auditLogs: (_, event) => event.data, + }), + }, + services: { + loadAuditLogs: () => getAuditLogs(), + }, + }, +) From 38955a874af6e2c7ed73232e72f21e5d335031d3 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Tue, 30 Aug 2022 21:13:14 +0000 Subject: [PATCH 02/28] Add base audit log component --- site/src/api/api.ts | 2 +- site/src/pages/AuditPage/AuditPage.tsx | 8 +- .../pages/AuditPage/AuditPageView.stories.tsx | 4 + site/src/pages/AuditPage/AuditPageView.tsx | 122 +++++++++++++----- .../pages/TemplatePage/TemplatePageView.tsx | 4 +- site/src/testHelpers/entities.ts | 4 +- site/src/theme/overrides.ts | 4 +- site/src/xServices/audit/auditXService.ts | 2 +- 8 files changed, 108 insertions(+), 42 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ed2c3b43ddee9..ef25e6c32a4a0 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -386,7 +386,7 @@ export const getEntitlements = async (): Promise => { return response.data } -interface AuditLog { +export interface AuditLog { readonly id: string readonly request_id: string readonly time: string diff --git a/site/src/pages/AuditPage/AuditPage.tsx b/site/src/pages/AuditPage/AuditPage.tsx index 1ee1f465b67cf..2e4fb303549ca 100644 --- a/site/src/pages/AuditPage/AuditPage.tsx +++ b/site/src/pages/AuditPage/AuditPage.tsx @@ -1,9 +1,13 @@ +import { useMachine } from "@xstate/react" import { FC } from "react" +import { auditMachine } from "xServices/audit/auditXService" import { AuditPageView } from "./AuditPageView" -// REMARK: This page is in-progress and hidden from users const AuditPage: FC = () => { - return + const [auditState] = useMachine(auditMachine) + const { auditLogs } = auditState.context + + return } export default AuditPage diff --git a/site/src/pages/AuditPage/AuditPageView.stories.tsx b/site/src/pages/AuditPage/AuditPageView.stories.tsx index 7ad3725855264..f5b0244617cc3 100644 --- a/site/src/pages/AuditPage/AuditPageView.stories.tsx +++ b/site/src/pages/AuditPage/AuditPageView.stories.tsx @@ -1,4 +1,5 @@ import { ComponentMeta, Story } from "@storybook/react" +import { MockAuditLog } from "testHelpers/entities" import { AuditPageView } from "./AuditPageView" export default { @@ -9,6 +10,9 @@ export default { const Template: Story = (args) => export const AuditPage = Template.bind({}) +AuditPage.args = { + auditLogs: [MockAuditLog, MockAuditLog], +} export const AuditPageSmallViewport = Template.bind({}) AuditPageSmallViewport.parameters = { diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index c284717fe2c9c..7cec07d5bcf11 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -1,10 +1,21 @@ import { makeStyles } from "@material-ui/core/styles" +import Table from "@material-ui/core/Table" +import TableBody from "@material-ui/core/TableBody" +import TableCell from "@material-ui/core/TableCell" +import TableContainer from "@material-ui/core/TableContainer" +import TableHead from "@material-ui/core/TableHead" +import TableRow from "@material-ui/core/TableRow" +import { AuditLog } from "api/api" import { CodeExample } from "components/CodeExample/CodeExample" import { Margins } from "components/Margins/Margins" import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "components/PageHeader/PageHeader" +import { Pill } from "components/Pill/Pill" import { Stack } from "components/Stack/Stack" +import { TableLoader } from "components/TableLoader/TableLoader" import { AuditHelpTooltip } from "components/Tooltips" +import { UserAvatar } from "components/UserAvatar/UserAvatar" import { FC } from "react" +import { createDayString } from "util/createDayString" export const Language = { title: "Audit", @@ -12,48 +23,95 @@ export const Language = { tooltipTitle: "Copy to clipboard and try the Coder CLI", } -export const AuditPageView: FC = () => { +export const AuditPageView: FC<{ auditLogs?: AuditLog[] }> = ({ auditLogs }) => { const styles = useStyles() return ( - - - - - {Language.title} - - - - {Language.subtitle} - - - + + } + > + + + {Language.title} + + + + {Language.subtitle} + + + + + + + Logs + + + + {auditLogs ? ( + auditLogs.map((log) => ( + + + + + +
+ + {log.user?.username} {log.action}{" "} + {log.resource.name} + + {createDayString(log.time)} +
+
+ + + + +
+ IP {log.ip} +
+
+ Agent {log.user_agent} +
+
+
+
+
+
+ )) + ) : ( + + )} +
+
+
) } 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", - }, + auditLogResume: { + ...theme.typography.body1, + fontFamily: "inherit", + display: "block", }, - headingStyles: { - paddingTop: "0px", - paddingBottom: "0px", + + auditLogTime: { + ...theme.typography.body2, + fontFamily: "inherit", + color: theme.palette.text.secondary, + display: "block", }, - codeExampleStyles: { - height: "fit-content", + + auditLogExtraInfo: { + ...theme.typography.body2, + fontFamily: "inherit", + color: theme.palette.text.secondary, }, })) diff --git a/site/src/pages/TemplatePage/TemplatePageView.tsx b/site/src/pages/TemplatePage/TemplatePageView.tsx index a2e25ef88fd4f..5641de67b7657 100644 --- a/site/src/pages/TemplatePage/TemplatePageView.tsx +++ b/site/src/pages/TemplatePage/TemplatePageView.tsx @@ -56,7 +56,7 @@ export const TemplatePageView: FC + <> > - + } > diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index deb93d6cad2f8..9902dae0686d6 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -637,8 +637,8 @@ export const makeMockApiError = ({ detail, validations, }: { - message?: "" - detail?: "" + message?: string + detail?: string validations?: FieldError[] }) => ({ response: { diff --git a/site/src/theme/overrides.ts b/site/src/theme/overrides.ts index 8541cc687558d..45a474fce761b 100644 --- a/site/src/theme/overrides.ts +++ b/site/src/theme/overrides.ts @@ -120,10 +120,10 @@ export const getOverrides = ({ palette, breakpoints }: Theme): Overrides => { padding: "12px 8px", // This targets the first+last td elements, and also the first+last elements // of a TableCellLink. - "&:not(:only-child):first-child, &:not(:only-child):first-child > a": { + "&:first-child, &:first-child > a": { paddingLeft: 32, }, - "&:not(:only-child):last-child, &:not(:only-child):last-child > a": { + "&:last-child, &:last-child > a": { paddingRight: 32, }, }, diff --git a/site/src/xServices/audit/auditXService.ts b/site/src/xServices/audit/auditXService.ts index defc75febd4f5..3d6565027e33a 100644 --- a/site/src/xServices/audit/auditXService.ts +++ b/site/src/xServices/audit/auditXService.ts @@ -7,7 +7,7 @@ export const auditMachine = createMachine( { id: "auditMachine", schema: { - context: {} as { auditLogs: AuditLogs }, + context: {} as { auditLogs?: AuditLogs }, services: {} as { loadAuditLogs: { data: AuditLogs From 785f67baf68461272e29bd2421fcc4df3542fe08 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 31 Aug 2022 14:47:56 +0000 Subject: [PATCH 03/28] Add basic diff --- site/src/components/Stack/Stack.tsx | 11 +- .../pages/AuditPage/AuditPageView.stories.tsx | 4 +- site/src/pages/AuditPage/AuditPageView.tsx | 181 ++++++++++++++---- site/src/testHelpers/entities.ts | 32 ++++ 4 files changed, 191 insertions(+), 37 deletions(-) diff --git a/site/src/components/Stack/Stack.tsx b/site/src/components/Stack/Stack.tsx index d5424e6a0bbcb..d12f4e5821a56 100644 --- a/site/src/components/Stack/Stack.tsx +++ b/site/src/components/Stack/Stack.tsx @@ -6,13 +6,13 @@ import { combineClasses } from "../../util/combineClasses" type Direction = "column" | "row" -export interface StackProps { +export type StackProps = { className?: string direction?: Direction spacing?: number alignItems?: CSSProperties["alignItems"] justifyContent?: CSSProperties["justifyContent"] -} +} & React.HTMLProps type StyleProps = Omit @@ -37,6 +37,7 @@ export const Stack: FC = ({ spacing = 2, alignItems, justifyContent, + ...divProps }) => { const styles = useStyles({ spacing, @@ -45,5 +46,9 @@ export const Stack: FC = ({ justifyContent, }) - return
{children}
+ return ( +
+ {children} +
+ ) } diff --git a/site/src/pages/AuditPage/AuditPageView.stories.tsx b/site/src/pages/AuditPage/AuditPageView.stories.tsx index f5b0244617cc3..b52fa6ff22dc8 100644 --- a/site/src/pages/AuditPage/AuditPageView.stories.tsx +++ b/site/src/pages/AuditPage/AuditPageView.stories.tsx @@ -1,5 +1,5 @@ import { ComponentMeta, Story } from "@storybook/react" -import { MockAuditLog } from "testHelpers/entities" +import { MockAuditLog, MockAuditLogWithDiff } from "testHelpers/entities" import { AuditPageView } from "./AuditPageView" export default { @@ -11,7 +11,7 @@ const Template: Story = (args) => export const AuditPage = Template.bind({}) AuditPage.args = { - auditLogs: [MockAuditLog, MockAuditLog], + auditLogs: [MockAuditLog, MockAuditLog, MockAuditLogWithDiff], } export const AuditPageSmallViewport = Template.bind({}) diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index 7cec07d5bcf11..1800efe9d6ce3 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -1,3 +1,4 @@ +import Collapse from "@material-ui/core/Collapse" import { makeStyles } from "@material-ui/core/styles" import Table from "@material-ui/core/Table" import TableBody from "@material-ui/core/TableBody" @@ -7,6 +8,7 @@ import TableHead from "@material-ui/core/TableHead" import TableRow from "@material-ui/core/TableRow" import { AuditLog } from "api/api" import { CodeExample } from "components/CodeExample/CodeExample" +import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows" import { Margins } from "components/Margins/Margins" import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "components/PageHeader/PageHeader" import { Pill } from "components/Pill/Pill" @@ -14,9 +16,94 @@ import { Stack } from "components/Stack/Stack" import { TableLoader } from "components/TableLoader/TableLoader" import { AuditHelpTooltip } from "components/Tooltips" import { UserAvatar } from "components/UserAvatar/UserAvatar" -import { FC } from "react" +import { FC, useState } from "react" import { createDayString } from "util/createDayString" +const AuditDiff = () => { + const styles = useStyles() + + return ( +
+
+
+
1
+
-
+
+ workspace_name: alice-workspace +
+
+
+
+
+
1
+
+
+
+ workspace_name: bruno-workspace +
+
+
+
+ ) +} + +const AuditLogRow: React.FC<{ auditLog: AuditLog }> = ({ auditLog }) => { + const styles = useStyles() + const [isDiffOpen, setIsDiffOpen] = useState(false) + + return ( + <> + setIsDiffOpen((v) => !v)} + onKeyDown={(event) => { + if (event.key === "Enter") { + setIsDiffOpen((v) => !v) + } + }} + > + + + +
+ + {auditLog.user?.username} {auditLog.action}{" "} + {auditLog.resource.name} + + {createDayString(auditLog.time)} +
+
+ + + + +
+ IP {auditLog.ip} +
+
+ Agent {auditLog.user_agent} +
+
+
+
+ + {isDiffOpen ? : } +
+ + + + + + ) +} + export const Language = { title: "Audit", subtitle: "View events in your audit log.", @@ -51,37 +138,10 @@ export const AuditPageView: FC<{ auditLogs?: AuditLog[] }> = ({ auditLogs }) => {auditLogs ? ( - auditLogs.map((log) => ( - - - - - -
- - {log.user?.username} {log.action}{" "} - {log.resource.name} - - {createDayString(log.time)} -
-
- - - - -
- IP {log.ip} -
-
- Agent {log.user_agent} -
-
-
-
+ auditLogs.map((auditLog) => ( + + + )) @@ -96,6 +156,19 @@ export const AuditPageView: FC<{ auditLogs?: AuditLog[] }> = ({ auditLogs }) => } const useStyles = makeStyles((theme) => ({ + auditLogCell: { + padding: "0 !important", + }, + + auditLogRow: { + padding: theme.spacing(2, 4), + cursor: "pointer", + }, + + auditLogRowInfo: { + flex: 1, + }, + auditLogResume: { ...theme.typography.body1, fontFamily: "inherit", @@ -114,4 +187,48 @@ const useStyles = makeStyles((theme) => ({ fontFamily: "inherit", color: theme.palette.text.secondary, }, + + diff: { + display: "flex", + alignItems: "flex-start", + fontSize: theme.typography.body2.fontSize, + borderTop: `1px solid ${theme.palette.divider}`, + }, + + diffOld: { + backgroundColor: theme.palette.error.dark, + color: theme.palette.error.contrastText, + flex: 1, + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + }, + + diffRow: { + display: "flex", + alignItems: "baseline", + }, + + diffLine: { + opacity: 0.5, + padding: theme.spacing(1), + width: theme.spacing(8), + textAlign: "right", + }, + + diffIcon: { + padding: theme.spacing(1), + width: theme.spacing(4), + textAlign: "center", + fontSize: theme.typography.body1.fontSize, + }, + + diffContent: {}, + + diffNew: { + backgroundColor: theme.palette.success.dark, + color: theme.palette.success.contrastText, + flex: 1, + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + }, })) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 9902dae0686d6..0f87d069a179c 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -703,3 +703,35 @@ export const MockAuditLog = { user: MockUser, resource: MockOrganization, } + +export const MockAuditLogWithDiff = { + id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", + request_id: "53bded77-7b9d-4e82-8771-991a34d759f9", + time: "2022-05-19T16:45:57.122Z", + organization_id: "fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0", + ip: "127.0.0.1", + user_agent: "browser", + resource_type: "organization", + resource_id: "ef8d1cf4-82de-4fd9-8980-047dad6d06b5", + resource_target: "Bruno's Org", + action: "write", + diff: { + workspace_name: { + old: "alice-workspace", + new: "aharvey", + }, + workspace_auto_off: { + old: true, + new: false, + }, + template_version_id: { + old: "fbd2116a-8961-4954-87ae-e4575bd29ce0", + new: "53bded77-7b9d-4e82-8771-991a34d759f9", + }, + }, + status_code: 200, + additional_fields: {}, + description: "Colin Adler updated the organization Bruno's Org", + user: MockUser, + resource: MockOrganization, +} From 74795382c4f36e0e35c13c4a9ab73b6e0dc2076a Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 31 Aug 2022 14:57:42 +0000 Subject: [PATCH 04/28] Check emtpy diffs --- site/src/pages/AuditPage/AuditPageView.tsx | 110 +++++++++++---------- 1 file changed, 60 insertions(+), 50 deletions(-) diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index 1800efe9d6ce3..a2d2726c9d094 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -49,58 +49,73 @@ const AuditDiff = () => { const AuditLogRow: React.FC<{ auditLog: AuditLog }> = ({ auditLog }) => { const styles = useStyles() const [isDiffOpen, setIsDiffOpen] = useState(false) + const diffs = Object.entries(auditLog.diff) + const shouldDisplayDiff = diffs.length > 1 + + const toggle = () => { + if (shouldDisplayDiff) { + setIsDiffOpen((v) => !v) + } + } return ( - <> - setIsDiffOpen((v) => !v)} - onKeyDown={(event) => { - if (event.key === "Enter") { - setIsDiffOpen((v) => !v) - } - }} - > + + { + if (event.key === "Enter") { + toggle() + } + }} > - - -
- - {auditLog.user?.username} {auditLog.action}{" "} - {auditLog.resource.name} - - {createDayString(auditLog.time)} -
-
- - - - + + +
- IP {auditLog.ip} -
-
- Agent {auditLog.user_agent} + + {auditLog.user?.username} {auditLog.action}{" "} + {auditLog.resource.name} + + {createDayString(auditLog.time)}
+ + + + +
+ IP {auditLog.ip} +
+
+ Agent {auditLog.user_agent} +
+
+
-
- {isDiffOpen ? : } -
+
+ {isDiffOpen ? : } +
+
- - - - + {shouldDisplayDiff && ( + + + + )} +
+
) } @@ -111,8 +126,6 @@ export const Language = { } export const AuditPageView: FC<{ auditLogs?: AuditLog[] }> = ({ auditLogs }) => { - const styles = useStyles() - return ( = ({ auditLogs }) => {auditLogs ? ( - auditLogs.map((auditLog) => ( - - - - - - )) + auditLogs.map((auditLog) => ) ) : ( )} @@ -162,7 +169,6 @@ const useStyles = makeStyles((theme) => ({ auditLogRow: { padding: theme.spacing(2, 4), - cursor: "pointer", }, auditLogRowInfo: { @@ -188,6 +194,10 @@ const useStyles = makeStyles((theme) => ({ color: theme.palette.text.secondary, }, + disabledDropdownIcon: { + opacity: 0.5, + }, + diff: { display: "flex", alignItems: "flex-start", From b30165a2ba3738b884d0d39dbba36e8d087f8244 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 31 Aug 2022 17:36:33 +0000 Subject: [PATCH 05/28] Improve human readable message --- site/src/api/api.ts | 11 +- site/src/pages/AuditPage/AuditPageView.tsx | 156 +++++++++++++++++---- site/src/testHelpers/entities.ts | 3 +- 3 files changed, 139 insertions(+), 31 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ef25e6c32a4a0..ff3481eef6ec0 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -395,18 +395,23 @@ export interface AuditLog { // eslint-disable-next-line @typescript-eslint/no-explicit-any readonly ip: any readonly user_agent: string - readonly resource_type: any + readonly resource_type: "organization" | "template" | "template_version" | "user" | "workspace" readonly resource_id: string readonly resource_target: string readonly action: any - readonly diff: any + readonly diff: Record readonly status_code: number // This is likely an enum in an external package ("encoding/json.RawMessage") readonly additional_fields: any readonly description: string readonly user?: User // This is likely an enum in an external package ("encoding/json.RawMessage") - readonly resource: any + readonly resource: + | TypesGen.Organization + | TypesGen.Template + | TypesGen.TemplateVersion + | TypesGen.User + | TypesGen.Workspace } export const getAuditLogs = async (): Promise => { diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index a2d2726c9d094..1d0027c9f1c2a 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -1,4 +1,5 @@ import Collapse from "@material-ui/core/Collapse" +import Link from "@material-ui/core/Link" import { makeStyles } from "@material-ui/core/styles" import Table from "@material-ui/core/Table" import TableBody from "@material-ui/core/TableBody" @@ -7,6 +8,7 @@ import TableContainer from "@material-ui/core/TableContainer" import TableHead from "@material-ui/core/TableHead" import TableRow from "@material-ui/core/TableRow" import { AuditLog } from "api/api" +import { Template, Workspace } from "api/typesGenerated" import { CodeExample } from "components/CodeExample/CodeExample" import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows" import { Margins } from "components/Margins/Margins" @@ -17,40 +19,124 @@ import { TableLoader } from "components/TableLoader/TableLoader" import { AuditHelpTooltip } from "components/Tooltips" import { UserAvatar } from "components/UserAvatar/UserAvatar" import { FC, useState } from "react" +import { Link as RouterLink } from "react-router-dom" +import { colors } from "theme/colors" +import { combineClasses } from "util/combineClasses" import { createDayString } from "util/createDayString" -const AuditDiff = () => { +const getDiffValue = (value: number | string | boolean) => { + if (typeof value === "string") { + return `"${value}"` + } + + return value.toString() +} + +const AuditDiff: React.FC<{ diff: AuditLog["diff"] }> = ({ diff }) => { const styles = useStyles() + const diffEntries = Object.entries(diff) return (
-
-
-
1
-
-
-
- workspace_name: alice-workspace +
+ {diffEntries.map(([attrName, valueDiff], index) => ( +
+
{index + 1}
+
-
+
+ {attrName}:{" "} + + {getDiffValue(valueDiff.old)} + +
-
+ ))}
-
-
-
1
-
+
-
- workspace_name: bruno-workspace +
+ {diffEntries.map(([attrName, valueDiff], index) => ( +
+
{index + 1}
+
+
+
+ {attrName}:{" "} + + {getDiffValue(valueDiff.new)} + +
-
+ ))}
) } +const getResourceLabel = (resource: AuditLog["resource"]): string => { + if ("name" in resource) { + return resource.name + } + + return resource.username +} + +const getResourceHref = ( + resource: AuditLog["resource"], + resourceType: AuditLog["resource_type"], +): string | undefined => { + switch (resourceType) { + case "user": + return `/users` + case "template": + return `/templates/${(resource as Template).name}` + case "workspace": + return `/workspaces/@${(resource as Workspace).owner_name}/${(resource as Workspace).name}` + case "organization": + return + } +} + +const ResourceLink: React.FC<{ + resource: AuditLog["resource"] + resourceType: AuditLog["resource_type"] +}> = ({ resource, resourceType }) => { + const href = getResourceHref(resource, resourceType) + const label = {getResourceLabel(resource)} + + if (!href) { + return label + } + + return ( + + {label} + + ) +} + +const actionLabelByAction: Record = { + create: "created", + write: "updated", + delete: "deleted", +} + +const resourceLabelByResourceType: Record = { + organization: "organization", + template: "template", + template_version: "template version", + user: "user", + workspace: "workspace", +} + +const readableActionMessage = (auditLog: AuditLog) => { + return `${actionLabelByAction[auditLog.action]} ${ + resourceLabelByResourceType[auditLog.resource_type] + }` +} + const AuditLogRow: React.FC<{ auditLog: AuditLog }> = ({ auditLog }) => { const styles = useStyles() const [isDiffOpen, setIsDiffOpen] = useState(false) const diffs = Object.entries(auditLog.diff) - const shouldDisplayDiff = diffs.length > 1 + const shouldDisplayDiff = diffs.length > 0 const toggle = () => { if (shouldDisplayDiff) { @@ -84,8 +170,11 @@ const AuditLogRow: React.FC<{ auditLog: AuditLog }> = ({ auditLog }) => {
- {auditLog.user?.username} {auditLog.action}{" "} - {auditLog.resource.name} + {auditLog.user?.username} {readableActionMessage(auditLog)}{" "} + {createDayString(auditLog.time)}
@@ -111,7 +200,7 @@ const AuditLogRow: React.FC<{ auditLog: AuditLog }> = ({ auditLog }) => { {shouldDisplayDiff && ( - + )} @@ -205,12 +294,16 @@ const useStyles = makeStyles((theme) => ({ borderTop: `1px solid ${theme.palette.divider}`, }, + diffColumn: { + flex: 1, + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2.5), + lineHeight: "160%", + }, + diffOld: { backgroundColor: theme.palette.error.dark, color: theme.palette.error.contrastText, - flex: 1, - paddingTop: theme.spacing(1), - paddingBottom: theme.spacing(1), }, diffRow: { @@ -220,13 +313,12 @@ const useStyles = makeStyles((theme) => ({ diffLine: { opacity: 0.5, - padding: theme.spacing(1), + width: theme.spacing(8), textAlign: "right", }, diffIcon: { - padding: theme.spacing(1), width: theme.spacing(4), textAlign: "center", fontSize: theme.typography.body1.fontSize, @@ -237,8 +329,18 @@ const useStyles = makeStyles((theme) => ({ diffNew: { backgroundColor: theme.palette.success.dark, color: theme.palette.success.contrastText, - flex: 1, - paddingTop: theme.spacing(1), - paddingBottom: theme.spacing(1), + }, + + diffValue: { + padding: 1, + borderRadius: theme.shape.borderRadius / 2, + }, + + diffValueOld: { + backgroundColor: colors.red[12], + }, + + diffValueNew: { + backgroundColor: colors.green[12], }, })) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 0f87d069a179c..dba03eeb22f61 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1,3 +1,4 @@ +import { AuditLog } from "api/api" import { FieldError } from "api/errors" import * as Types from "../api/types" import * as TypesGen from "../api/typesGenerated" @@ -685,7 +686,7 @@ export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { }, } -export const MockAuditLog = { +export const MockAuditLog: AuditLog = { id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", request_id: "53bded77-7b9d-4e82-8771-991a34d759f9", time: "2022-05-19T16:45:57.122Z", From f9a7ed77f05c066b28c2aec57f77e4ce988bf1ab Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 31 Aug 2022 17:51:41 +0000 Subject: [PATCH 06/28] Move types --- site/src/api/api.ts | 32 ++----------------- site/src/api/typesGenerated.ts | 24 ++++++++++++++ .../pages/AuditPage/AuditPageView.stories.tsx | 4 +-- site/src/pages/AuditPage/AuditPageView.tsx | 5 ++- site/src/testHelpers/entities.ts | 28 ++++------------ 5 files changed, 37 insertions(+), 56 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ff3481eef6ec0..000a9d967264d 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -4,7 +4,6 @@ import { MockAuditLog } from "testHelpers/entities" import * as Types from "./types" import { WorkspaceBuildTransition } from "./types" import * as TypesGen from "./typesGenerated" -import { User } from "./typesGenerated" const CONTENT_TYPE_JSON: AxiosRequestHeaders = { "Content-Type": "application/json", @@ -386,36 +385,9 @@ export const getEntitlements = async (): Promise => { return response.data } -export interface AuditLog { - readonly id: string - readonly request_id: string - readonly time: string - readonly organization_id: string - // Named type "net/netip.Addr" unknown, using "any" - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly ip: any - readonly user_agent: string - readonly resource_type: "organization" | "template" | "template_version" | "user" | "workspace" - readonly resource_id: string - readonly resource_target: string - readonly action: any - readonly diff: Record - readonly status_code: number - // This is likely an enum in an external package ("encoding/json.RawMessage") - readonly additional_fields: any - readonly description: string - readonly user?: User - // This is likely an enum in an external package ("encoding/json.RawMessage") - readonly resource: - | TypesGen.Organization - | TypesGen.Template - | TypesGen.TemplateVersion - | TypesGen.User - | TypesGen.Workspace -} - -export const getAuditLogs = async (): Promise => { +export const getAuditLogs = async (): Promise => { return [MockAuditLog] + // TODO: Uncomment this to get the data from the API instead of mock // const response = await axios.get("/api/v2/audit") // return response.data } diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index a399774d1011b..25e428ad0eb54 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -620,3 +620,27 @@ export type WorkspaceAgentStatus = "connected" | "connecting" | "disconnected" // From codersdk/workspacebuilds.go export type WorkspaceTransition = "delete" | "start" | "stop" + +// TODO: Remove this when the generated types work for AuditLogs +export interface AuditLog { + readonly id: string + readonly request_id: string + readonly time: string + readonly organization_id: string + // Named type "net/netip.Addr" unknown, using "any" + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly ip: any + readonly user_agent: string + readonly resource_type: "organization" | "template" | "template_version" | "user" | "workspace" + readonly resource_id: string + readonly resource_target: string + readonly action: "write" | "create" | "delete" + readonly diff: Record + readonly status_code: number + // This is likely an enum in an external package ("encoding/json.RawMessage") + readonly additional_fields: Record + readonly description: string + readonly user?: User + // This is likely an enum in an external package ("encoding/json.RawMessage") + readonly resource: Organization | Template | TemplateVersion | User | Workspace +} diff --git a/site/src/pages/AuditPage/AuditPageView.stories.tsx b/site/src/pages/AuditPage/AuditPageView.stories.tsx index b52fa6ff22dc8..3687970e61800 100644 --- a/site/src/pages/AuditPage/AuditPageView.stories.tsx +++ b/site/src/pages/AuditPage/AuditPageView.stories.tsx @@ -1,5 +1,5 @@ import { ComponentMeta, Story } from "@storybook/react" -import { MockAuditLog, MockAuditLogWithDiff } from "testHelpers/entities" +import { MockAuditLog, MockAuditLog2 } from "testHelpers/entities" import { AuditPageView } from "./AuditPageView" export default { @@ -11,7 +11,7 @@ const Template: Story = (args) => export const AuditPage = Template.bind({}) AuditPage.args = { - auditLogs: [MockAuditLog, MockAuditLog, MockAuditLogWithDiff], + auditLogs: [MockAuditLog, MockAuditLog2], } export const AuditPageSmallViewport = Template.bind({}) diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index 1d0027c9f1c2a..6c37608b3b27b 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -7,8 +7,7 @@ import TableCell from "@material-ui/core/TableCell" import TableContainer from "@material-ui/core/TableContainer" import TableHead from "@material-ui/core/TableHead" import TableRow from "@material-ui/core/TableRow" -import { AuditLog } from "api/api" -import { Template, Workspace } from "api/typesGenerated" +import { AuditLog, Template, Workspace } from "api/typesGenerated" import { CodeExample } from "components/CodeExample/CodeExample" import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows" import { Margins } from "components/Margins/Margins" @@ -113,7 +112,7 @@ const ResourceLink: React.FC<{ } const actionLabelByAction: Record = { - create: "created", + create: "created a new", write: "updated", delete: "deleted", } diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index dba03eeb22f61..dff5678bbf7be 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1,4 +1,3 @@ -import { AuditLog } from "api/api" import { FieldError } from "api/errors" import * as Types from "../api/types" import * as TypesGen from "../api/typesGenerated" @@ -686,14 +685,14 @@ export const MockEntitlementsWithAuditLog: TypesGen.Entitlements = { }, } -export const MockAuditLog: AuditLog = { +export const MockAuditLog: TypesGen.AuditLog = { id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", request_id: "53bded77-7b9d-4e82-8771-991a34d759f9", time: "2022-05-19T16:45:57.122Z", organization_id: "fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0", ip: "127.0.0.1", user_agent: "browser", - resource_type: "organization", + resource_type: "workspace", resource_id: "ef8d1cf4-82de-4fd9-8980-047dad6d06b5", resource_target: "Bruno's Org", action: "create", @@ -702,24 +701,16 @@ export const MockAuditLog: AuditLog = { additional_fields: {}, description: "Colin Adler updated the organization Bruno's Org", user: MockUser, - resource: MockOrganization, + resource: MockWorkspace, } -export const MockAuditLogWithDiff = { - id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", - request_id: "53bded77-7b9d-4e82-8771-991a34d759f9", - time: "2022-05-19T16:45:57.122Z", - organization_id: "fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0", - ip: "127.0.0.1", - user_agent: "browser", - resource_type: "organization", - resource_id: "ef8d1cf4-82de-4fd9-8980-047dad6d06b5", - resource_target: "Bruno's Org", +export const MockAuditLog2: TypesGen.AuditLog = { + ...MockAuditLog, action: "write", diff: { workspace_name: { - old: "alice-workspace", - new: "aharvey", + old: "old-workspace-name", + new: MockWorkspace.name, }, workspace_auto_off: { old: true, @@ -730,9 +721,4 @@ export const MockAuditLogWithDiff = { new: "53bded77-7b9d-4e82-8771-991a34d759f9", }, }, - status_code: 200, - additional_fields: {}, - description: "Colin Adler updated the organization Bruno's Org", - user: MockUser, - resource: MockOrganization, } From 9db338571a21ea217a45aff03aa02e3dbcc8034f Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 31 Aug 2022 18:07:58 +0000 Subject: [PATCH 07/28] Extract components and add storybook --- .../components/AuditLogRow/AuditLogDiff.tsx | 109 +++++++ .../AuditLogRow/AuditLogRow.stories.tsx | 40 +++ .../components/AuditLogRow/AuditLogRow.tsx | 198 ++++++++++++ site/src/pages/AuditPage/AuditPageView.tsx | 293 +----------------- 4 files changed, 350 insertions(+), 290 deletions(-) create mode 100644 site/src/components/AuditLogRow/AuditLogDiff.tsx create mode 100644 site/src/components/AuditLogRow/AuditLogRow.stories.tsx create mode 100644 site/src/components/AuditLogRow/AuditLogRow.tsx diff --git a/site/src/components/AuditLogRow/AuditLogDiff.tsx b/site/src/components/AuditLogRow/AuditLogDiff.tsx new file mode 100644 index 0000000000000..66a2642e765b3 --- /dev/null +++ b/site/src/components/AuditLogRow/AuditLogDiff.tsx @@ -0,0 +1,109 @@ +import { makeStyles } from "@material-ui/core/styles" +import { AuditLog } from "api/typesGenerated" +import { colors } from "theme/colors" +import { combineClasses } from "util/combineClasses" + +const getDiffValue = (value: number | string | boolean) => { + if (typeof value === "string") { + return `"${value}"` + } + + return value.toString() +} + +export const AuditDiff: React.FC<{ diff: AuditLog["diff"] }> = ({ diff }) => { + const styles = useStyles() + const diffEntries = Object.entries(diff) + + return ( +
+
+ {diffEntries.map(([attrName, valueDiff], index) => ( +
+
{index + 1}
+
-
+
+ {attrName}:{" "} + + {getDiffValue(valueDiff.old)} + +
+
+ ))} +
+
+ {diffEntries.map(([attrName, valueDiff], index) => ( +
+
{index + 1}
+
+
+
+ {attrName}:{" "} + + {getDiffValue(valueDiff.new)} + +
+
+ ))} +
+
+ ) +} + +const useStyles = makeStyles((theme) => ({ + diff: { + display: "flex", + alignItems: "flex-start", + fontSize: theme.typography.body2.fontSize, + borderTop: `1px solid ${theme.palette.divider}`, + }, + + diffColumn: { + flex: 1, + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2.5), + lineHeight: "160%", + }, + + diffOld: { + backgroundColor: theme.palette.error.dark, + color: theme.palette.error.contrastText, + }, + + diffRow: { + display: "flex", + alignItems: "baseline", + }, + + diffLine: { + opacity: 0.5, + + width: theme.spacing(8), + textAlign: "right", + }, + + diffIcon: { + width: theme.spacing(4), + textAlign: "center", + fontSize: theme.typography.body1.fontSize, + }, + + diffContent: {}, + + diffNew: { + backgroundColor: theme.palette.success.dark, + color: theme.palette.success.contrastText, + }, + + diffValue: { + padding: 1, + borderRadius: theme.shape.borderRadius / 2, + }, + + diffValueOld: { + backgroundColor: colors.red[12], + }, + + diffValueNew: { + backgroundColor: colors.green[12], + }, +})) diff --git a/site/src/components/AuditLogRow/AuditLogRow.stories.tsx b/site/src/components/AuditLogRow/AuditLogRow.stories.tsx new file mode 100644 index 0000000000000..bb6c03b083126 --- /dev/null +++ b/site/src/components/AuditLogRow/AuditLogRow.stories.tsx @@ -0,0 +1,40 @@ +import Table from "@material-ui/core/Table" +import TableBody from "@material-ui/core/TableBody" +import TableCell from "@material-ui/core/TableCell" +import TableContainer from "@material-ui/core/TableContainer" +import TableHead from "@material-ui/core/TableHead" +import TableRow from "@material-ui/core/TableRow" +import { ComponentMeta, Story } from "@storybook/react" +import { MockAuditLog, MockAuditLog2 } from "testHelpers/entities" +import { AuditLogRow, AuditLogRowProps } from "./AuditLogRow" + +export default { + title: "components/AuditLogRow", + component: AuditLogRow, +} as ComponentMeta + +const Template: Story = (args) => ( + + + + + Logs + + + + + +
+
+) + +export const NoDiff = Template.bind({}) +NoDiff.args = { + auditLog: MockAuditLog, +} + +export const WithDiff = Template.bind({}) +WithDiff.args = { + auditLog: MockAuditLog2, + defaultIsDiffOpen: true, +} diff --git a/site/src/components/AuditLogRow/AuditLogRow.tsx b/site/src/components/AuditLogRow/AuditLogRow.tsx new file mode 100644 index 0000000000000..f98cc2ff3eda9 --- /dev/null +++ b/site/src/components/AuditLogRow/AuditLogRow.tsx @@ -0,0 +1,198 @@ +import Collapse from "@material-ui/core/Collapse" +import Link from "@material-ui/core/Link" +import { makeStyles } from "@material-ui/core/styles" +import TableCell from "@material-ui/core/TableCell" +import TableRow from "@material-ui/core/TableRow" +import { AuditLog, Template, Workspace } from "api/typesGenerated" +import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows" +import { Pill } from "components/Pill/Pill" +import { Stack } from "components/Stack/Stack" +import { UserAvatar } from "components/UserAvatar/UserAvatar" +import { useState } from "react" +import { Link as RouterLink } from "react-router-dom" +import { createDayString } from "util/createDayString" +import { AuditDiff } from "./AuditLogDiff" + +const getResourceLabel = (resource: AuditLog["resource"]): string => { + if ("name" in resource) { + return resource.name + } + + return resource.username +} + +const getResourceHref = ( + resource: AuditLog["resource"], + resourceType: AuditLog["resource_type"], +): string | undefined => { + switch (resourceType) { + case "user": + return `/users` + case "template": + return `/templates/${(resource as Template).name}` + case "workspace": + return `/workspaces/@${(resource as Workspace).owner_name}/${(resource as Workspace).name}` + case "organization": + return + } +} + +const ResourceLink: React.FC<{ + resource: AuditLog["resource"] + resourceType: AuditLog["resource_type"] +}> = ({ resource, resourceType }) => { + const href = getResourceHref(resource, resourceType) + const label = {getResourceLabel(resource)} + + if (!href) { + return label + } + + return ( + + {label} + + ) +} + +const actionLabelByAction: Record = { + create: "created a new", + write: "updated", + delete: "deleted", +} + +const resourceLabelByResourceType: Record = { + organization: "organization", + template: "template", + template_version: "template version", + user: "user", + workspace: "workspace", +} + +const readableActionMessage = (auditLog: AuditLog) => { + return `${actionLabelByAction[auditLog.action]} ${ + resourceLabelByResourceType[auditLog.resource_type] + }` +} + +export interface AuditLogRowProps { + auditLog: AuditLog + // Useful for Storybook + defaultIsDiffOpen?: boolean +} + +export const AuditLogRow: React.FC = ({ + auditLog, + defaultIsDiffOpen = false, +}) => { + const styles = useStyles() + const [isDiffOpen, setIsDiffOpen] = useState(defaultIsDiffOpen) + const diffs = Object.entries(auditLog.diff) + const shouldDisplayDiff = diffs.length > 0 + + const toggle = () => { + if (shouldDisplayDiff) { + setIsDiffOpen((v) => !v) + } + } + + return ( + + + { + if (event.key === "Enter") { + toggle() + } + }} + > + + + +
+ + {auditLog.user?.username} {readableActionMessage(auditLog)}{" "} + + + {createDayString(auditLog.time)} +
+
+ + + + +
+ IP {auditLog.ip} +
+
+ Agent {auditLog.user_agent} +
+
+
+
+ +
+ {isDiffOpen ? : } +
+
+ + {shouldDisplayDiff && ( + + + + )} +
+
+ ) +} + +const useStyles = makeStyles((theme) => ({ + auditLogCell: { + padding: "0 !important", + }, + + auditLogRow: { + padding: theme.spacing(2, 4), + }, + + auditLogRowInfo: { + flex: 1, + }, + + auditLogResume: { + ...theme.typography.body1, + fontFamily: "inherit", + display: "block", + }, + + auditLogTime: { + ...theme.typography.body2, + fontFamily: "inherit", + color: theme.palette.text.secondary, + display: "block", + }, + + auditLogExtraInfo: { + ...theme.typography.body2, + fontFamily: "inherit", + color: theme.palette.text.secondary, + }, + + disabledDropdownIcon: { + opacity: 0.5, + }, +})) diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index 6c37608b3b27b..66c4ec6cb5afb 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -1,211 +1,18 @@ -import Collapse from "@material-ui/core/Collapse" -import Link from "@material-ui/core/Link" -import { makeStyles } from "@material-ui/core/styles" import Table from "@material-ui/core/Table" import TableBody from "@material-ui/core/TableBody" import TableCell from "@material-ui/core/TableCell" import TableContainer from "@material-ui/core/TableContainer" import TableHead from "@material-ui/core/TableHead" import TableRow from "@material-ui/core/TableRow" -import { AuditLog, Template, Workspace } from "api/typesGenerated" +import { AuditLog } from "api/typesGenerated" +import { AuditLogRow } from "components/AuditLogRow/AuditLogRow" import { CodeExample } from "components/CodeExample/CodeExample" -import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows" import { Margins } from "components/Margins/Margins" import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "components/PageHeader/PageHeader" -import { Pill } from "components/Pill/Pill" import { Stack } from "components/Stack/Stack" import { TableLoader } from "components/TableLoader/TableLoader" import { AuditHelpTooltip } from "components/Tooltips" -import { UserAvatar } from "components/UserAvatar/UserAvatar" -import { FC, useState } from "react" -import { Link as RouterLink } from "react-router-dom" -import { colors } from "theme/colors" -import { combineClasses } from "util/combineClasses" -import { createDayString } from "util/createDayString" - -const getDiffValue = (value: number | string | boolean) => { - if (typeof value === "string") { - return `"${value}"` - } - - return value.toString() -} - -const AuditDiff: React.FC<{ diff: AuditLog["diff"] }> = ({ diff }) => { - const styles = useStyles() - const diffEntries = Object.entries(diff) - - return ( -
-
- {diffEntries.map(([attrName, valueDiff], index) => ( -
-
{index + 1}
-
-
-
- {attrName}:{" "} - - {getDiffValue(valueDiff.old)} - -
-
- ))} -
-
- {diffEntries.map(([attrName, valueDiff], index) => ( -
-
{index + 1}
-
+
-
- {attrName}:{" "} - - {getDiffValue(valueDiff.new)} - -
-
- ))} -
-
- ) -} - -const getResourceLabel = (resource: AuditLog["resource"]): string => { - if ("name" in resource) { - return resource.name - } - - return resource.username -} - -const getResourceHref = ( - resource: AuditLog["resource"], - resourceType: AuditLog["resource_type"], -): string | undefined => { - switch (resourceType) { - case "user": - return `/users` - case "template": - return `/templates/${(resource as Template).name}` - case "workspace": - return `/workspaces/@${(resource as Workspace).owner_name}/${(resource as Workspace).name}` - case "organization": - return - } -} - -const ResourceLink: React.FC<{ - resource: AuditLog["resource"] - resourceType: AuditLog["resource_type"] -}> = ({ resource, resourceType }) => { - const href = getResourceHref(resource, resourceType) - const label = {getResourceLabel(resource)} - - if (!href) { - return label - } - - return ( - - {label} - - ) -} - -const actionLabelByAction: Record = { - create: "created a new", - write: "updated", - delete: "deleted", -} - -const resourceLabelByResourceType: Record = { - organization: "organization", - template: "template", - template_version: "template version", - user: "user", - workspace: "workspace", -} - -const readableActionMessage = (auditLog: AuditLog) => { - return `${actionLabelByAction[auditLog.action]} ${ - resourceLabelByResourceType[auditLog.resource_type] - }` -} - -const AuditLogRow: React.FC<{ auditLog: AuditLog }> = ({ auditLog }) => { - const styles = useStyles() - const [isDiffOpen, setIsDiffOpen] = useState(false) - const diffs = Object.entries(auditLog.diff) - const shouldDisplayDiff = diffs.length > 0 - - const toggle = () => { - if (shouldDisplayDiff) { - setIsDiffOpen((v) => !v) - } - } - - return ( - - - { - if (event.key === "Enter") { - toggle() - } - }} - > - - - -
- - {auditLog.user?.username} {readableActionMessage(auditLog)}{" "} - - - {createDayString(auditLog.time)} -
-
- - - - -
- IP {auditLog.ip} -
-
- Agent {auditLog.user_agent} -
-
-
-
- -
- {isDiffOpen ? : } -
-
- - {shouldDisplayDiff && ( - - - - )} -
-
- ) -} +import { FC } from "react" export const Language = { title: "Audit", @@ -249,97 +56,3 @@ export const AuditPageView: FC<{ auditLogs?: AuditLog[] }> = ({ auditLogs }) => ) } - -const useStyles = makeStyles((theme) => ({ - auditLogCell: { - padding: "0 !important", - }, - - auditLogRow: { - padding: theme.spacing(2, 4), - }, - - auditLogRowInfo: { - flex: 1, - }, - - auditLogResume: { - ...theme.typography.body1, - fontFamily: "inherit", - display: "block", - }, - - auditLogTime: { - ...theme.typography.body2, - fontFamily: "inherit", - color: theme.palette.text.secondary, - display: "block", - }, - - auditLogExtraInfo: { - ...theme.typography.body2, - fontFamily: "inherit", - color: theme.palette.text.secondary, - }, - - disabledDropdownIcon: { - opacity: 0.5, - }, - - diff: { - display: "flex", - alignItems: "flex-start", - fontSize: theme.typography.body2.fontSize, - borderTop: `1px solid ${theme.palette.divider}`, - }, - - diffColumn: { - flex: 1, - paddingTop: theme.spacing(2), - paddingBottom: theme.spacing(2.5), - lineHeight: "160%", - }, - - diffOld: { - backgroundColor: theme.palette.error.dark, - color: theme.palette.error.contrastText, - }, - - diffRow: { - display: "flex", - alignItems: "baseline", - }, - - diffLine: { - opacity: 0.5, - - width: theme.spacing(8), - textAlign: "right", - }, - - diffIcon: { - width: theme.spacing(4), - textAlign: "center", - fontSize: theme.typography.body1.fontSize, - }, - - diffContent: {}, - - diffNew: { - backgroundColor: theme.palette.success.dark, - color: theme.palette.success.contrastText, - }, - - diffValue: { - padding: 1, - borderRadius: theme.shape.borderRadius / 2, - }, - - diffValueOld: { - backgroundColor: colors.red[12], - }, - - diffValueNew: { - backgroundColor: colors.green[12], - }, -})) From 294dcebd7ead6f7b958c388bf944a75244249c8a Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 31 Aug 2022 18:16:45 +0000 Subject: [PATCH 08/28] Fix status pill --- .../components/AuditLogRow/AuditLogRow.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/site/src/components/AuditLogRow/AuditLogRow.tsx b/site/src/components/AuditLogRow/AuditLogRow.tsx index f98cc2ff3eda9..188f54ae941e1 100644 --- a/site/src/components/AuditLogRow/AuditLogRow.tsx +++ b/site/src/components/AuditLogRow/AuditLogRow.tsx @@ -8,11 +8,23 @@ import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownA import { Pill } from "components/Pill/Pill" import { Stack } from "components/Stack/Stack" import { UserAvatar } from "components/UserAvatar/UserAvatar" -import { useState } from "react" +import { ComponentProps, useState } from "react" import { Link as RouterLink } from "react-router-dom" import { createDayString } from "util/createDayString" import { AuditDiff } from "./AuditLogDiff" +const pillTypeByHttpStatus = (httpStatus: number): ComponentProps["type"] => { + if (httpStatus >= 300 && httpStatus < 500) { + return "warning" + } + + if (httpStatus > 500) { + return "error" + } + + return "success" +} + const getResourceLabel = (resource: AuditLog["resource"]): string => { if ("name" in resource) { return resource.name @@ -133,7 +145,10 @@ export const AuditLogRow: React.FC = ({ - +
IP {auditLog.ip} From 1b0ed592aa7cdd3926ec9a8b02969777d90a567f Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 31 Aug 2022 18:31:58 +0000 Subject: [PATCH 09/28] Add tests to check if the audit logs are showing --- site/src/api/api.ts | 7 ++----- .../src/components/AuditLogRow/AuditLogRow.tsx | 6 +++++- site/src/pages/AuditPage/AuditPage.test.tsx | 18 +++++++++++++++++- site/src/testHelpers/entities.ts | 1 + site/src/testHelpers/handlers.ts | 2 +- site/src/xServices/audit/auditXService.ts | 1 + 6 files changed, 27 insertions(+), 8 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 000a9d967264d..9272d032266d9 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1,6 +1,5 @@ import axios, { AxiosRequestHeaders } from "axios" import dayjs from "dayjs" -import { MockAuditLog } from "testHelpers/entities" import * as Types from "./types" import { WorkspaceBuildTransition } from "./types" import * as TypesGen from "./typesGenerated" @@ -386,8 +385,6 @@ export const getEntitlements = async (): Promise => { } export const getAuditLogs = async (): Promise => { - return [MockAuditLog] - // TODO: Uncomment this to get the data from the API instead of mock - // const response = await axios.get("/api/v2/audit") - // return response.data + const response = await axios.get("/api/v2/audit") + return response.data } diff --git a/site/src/components/AuditLogRow/AuditLogRow.tsx b/site/src/components/AuditLogRow/AuditLogRow.tsx index 188f54ae941e1..3ecb629dc1071 100644 --- a/site/src/components/AuditLogRow/AuditLogRow.tsx +++ b/site/src/components/AuditLogRow/AuditLogRow.tsx @@ -109,7 +109,11 @@ export const AuditLogRow: React.FC = ({ } return ( - + { + beforeEach(() => { + // Mocking the dayjs module within the createDayString file + const mock = jest.spyOn(CreateDayString, "createDayString") + mock.mockImplementation(() => "a minute ago") + }) + it("renders a page with a title and subtitle", async () => { // When render() @@ -29,4 +36,13 @@ describe("AuditPage", () => { fireEvent.mouseOver(copyIcon) expect(await screen.findByText(AuditViewLanguage.tooltipTitle)).toBeInTheDocument() }) + + it("shows the audit logs", async () => { + // When + render() + + // Then + await screen.findByTestId(`audit-log-row-${MockAuditLog.id}`) + screen.getByTestId(`audit-log-row-${MockAuditLog2.id}`) + }) }) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index dff5678bbf7be..00a52fc6e7c03 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -706,6 +706,7 @@ export const MockAuditLog: TypesGen.AuditLog = { export const MockAuditLog2: TypesGen.AuditLog = { ...MockAuditLog, + id: "53bded77-7b9d-4e82-8771-991a34d759f9", action: "write", diff: { workspace_name: { diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 2f88cd80e84c9..5c76aa4834f4a 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -150,6 +150,6 @@ export const handlers = [ // Audit rest.get("/api/v2/audit", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(M.MockAuditLog)) + return res(ctx.status(200), ctx.json([M.MockAuditLog, M.MockAuditLog2])) }), ] diff --git a/site/src/xServices/audit/auditXService.ts b/site/src/xServices/audit/auditXService.ts index 3d6565027e33a..b610a9b9f8751 100644 --- a/site/src/xServices/audit/auditXService.ts +++ b/site/src/xServices/audit/auditXService.ts @@ -15,6 +15,7 @@ export const auditMachine = createMachine( }, }, tsTypes: {} as import("./auditXService.typegen").Typegen0, + initial: "loadingLogs", states: { loadingLogs: { invoke: { From 6cf89a44d68ceaac2cce81b9a163ab467f961056 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 31 Aug 2022 19:04:03 +0000 Subject: [PATCH 10/28] Address PR review --- site/src/components/AuditLogRow/AuditLogDiff.tsx | 2 +- site/src/components/AuditLogRow/AuditLogRow.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/components/AuditLogRow/AuditLogDiff.tsx b/site/src/components/AuditLogRow/AuditLogDiff.tsx index 66a2642e765b3..3d48ff4c46dbb 100644 --- a/site/src/components/AuditLogRow/AuditLogDiff.tsx +++ b/site/src/components/AuditLogRow/AuditLogDiff.tsx @@ -11,7 +11,7 @@ const getDiffValue = (value: number | string | boolean) => { return value.toString() } -export const AuditDiff: React.FC<{ diff: AuditLog["diff"] }> = ({ diff }) => { +export const AuditLogDiff: React.FC<{ diff: AuditLog["diff"] }> = ({ diff }) => { const styles = useStyles() const diffEntries = Object.entries(diff) diff --git a/site/src/components/AuditLogRow/AuditLogRow.tsx b/site/src/components/AuditLogRow/AuditLogRow.tsx index 3ecb629dc1071..d6f696c9f5705 100644 --- a/site/src/components/AuditLogRow/AuditLogRow.tsx +++ b/site/src/components/AuditLogRow/AuditLogRow.tsx @@ -11,14 +11,14 @@ import { UserAvatar } from "components/UserAvatar/UserAvatar" import { ComponentProps, useState } from "react" import { Link as RouterLink } from "react-router-dom" import { createDayString } from "util/createDayString" -import { AuditDiff } from "./AuditLogDiff" +import { AuditLogDiff } from "./AuditLogDiff" const pillTypeByHttpStatus = (httpStatus: number): ComponentProps["type"] => { if (httpStatus >= 300 && httpStatus < 500) { return "warning" } - if (httpStatus > 500) { + if (httpStatus >= 500) { return "error" } @@ -171,7 +171,7 @@ export const AuditLogRow: React.FC = ({ {shouldDisplayDiff && ( - + )} From a5da88b577fbe338a356087e5157a305671b90fd Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 31 Aug 2022 19:15:01 +0000 Subject: [PATCH 11/28] Add i18n --- site/src/components/AuditLogRow/AuditLogRow.tsx | 7 ++++--- site/src/i18n/en/auditLog.json | 7 +++++++ site/src/i18n/en/index.ts | 2 ++ 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 site/src/i18n/en/auditLog.json diff --git a/site/src/components/AuditLogRow/AuditLogRow.tsx b/site/src/components/AuditLogRow/AuditLogRow.tsx index d6f696c9f5705..3778cf0b512db 100644 --- a/site/src/components/AuditLogRow/AuditLogRow.tsx +++ b/site/src/components/AuditLogRow/AuditLogRow.tsx @@ -8,6 +8,7 @@ import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownA import { Pill } from "components/Pill/Pill" import { Stack } from "components/Stack/Stack" import { UserAvatar } from "components/UserAvatar/UserAvatar" +import { t } from "i18next" import { ComponentProps, useState } from "react" import { Link as RouterLink } from "react-router-dom" import { createDayString } from "util/createDayString" @@ -68,9 +69,9 @@ const ResourceLink: React.FC<{ } const actionLabelByAction: Record = { - create: "created a new", - write: "updated", - delete: "deleted", + create: t("actions.create", { ns: "auditLog" }), + write: t("actions.write", { ns: "auditLog" }), + delete: t("actions.delete", { ns: "auditLog" }), } const resourceLabelByResourceType: Record = { diff --git a/site/src/i18n/en/auditLog.json b/site/src/i18n/en/auditLog.json new file mode 100644 index 0000000000000..4d118ad85ada4 --- /dev/null +++ b/site/src/i18n/en/auditLog.json @@ -0,0 +1,7 @@ +{ + "actions": { + "create": "created a new", + "write": "updated", + "delete": "deleted" + } +} diff --git a/site/src/i18n/en/index.ts b/site/src/i18n/en/index.ts index ffaa949384e91..b29e9a8d183ab 100644 --- a/site/src/i18n/en/index.ts +++ b/site/src/i18n/en/index.ts @@ -1,7 +1,9 @@ +import auditLog from "./auditLog.json" import common from "./common.json" import workspacePage from "./workspacePage.json" export const en = { common, workspacePage, + auditLog, } From a0ce84edc1f8c7043bf396c6424d4ebceb88548a Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 31 Aug 2022 23:24:35 +0000 Subject: [PATCH 12/28] Fix audit log row to match the new API types --- site/src/api/typesGenerated.ts | 24 --------- .../components/AuditLogRow/AuditLogRow.tsx | 51 +------------------ 2 files changed, 2 insertions(+), 73 deletions(-) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 790baed98a1a6..655787e0abb72 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -667,27 +667,3 @@ export type WorkspaceAgentStatus = "connected" | "connecting" | "disconnected" // From codersdk/workspacebuilds.go export type WorkspaceTransition = "delete" | "start" | "stop" - -// TODO: Remove this when the generated types work for AuditLogs -export interface AuditLog { - readonly id: string - readonly request_id: string - readonly time: string - readonly organization_id: string - // Named type "net/netip.Addr" unknown, using "any" - // eslint-disable-next-line @typescript-eslint/no-explicit-any - readonly ip: any - readonly user_agent: string - readonly resource_type: "organization" | "template" | "template_version" | "user" | "workspace" - readonly resource_id: string - readonly resource_target: string - readonly action: "write" | "create" | "delete" - readonly diff: Record - readonly status_code: number - // This is likely an enum in an external package ("encoding/json.RawMessage") - readonly additional_fields: Record - readonly description: string - readonly user?: User - // This is likely an enum in an external package ("encoding/json.RawMessage") - readonly resource: Organization | Template | TemplateVersion | User | Workspace -} diff --git a/site/src/components/AuditLogRow/AuditLogRow.tsx b/site/src/components/AuditLogRow/AuditLogRow.tsx index 3778cf0b512db..3a6b0682aee59 100644 --- a/site/src/components/AuditLogRow/AuditLogRow.tsx +++ b/site/src/components/AuditLogRow/AuditLogRow.tsx @@ -1,16 +1,14 @@ import Collapse from "@material-ui/core/Collapse" -import Link from "@material-ui/core/Link" import { makeStyles } from "@material-ui/core/styles" import TableCell from "@material-ui/core/TableCell" import TableRow from "@material-ui/core/TableRow" -import { AuditLog, Template, Workspace } from "api/typesGenerated" +import { AuditLog } from "api/typesGenerated" import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows" import { Pill } from "components/Pill/Pill" import { Stack } from "components/Stack/Stack" import { UserAvatar } from "components/UserAvatar/UserAvatar" import { t } from "i18next" import { ComponentProps, useState } from "react" -import { Link as RouterLink } from "react-router-dom" import { createDayString } from "util/createDayString" import { AuditLogDiff } from "./AuditLogDiff" @@ -26,48 +24,6 @@ const pillTypeByHttpStatus = (httpStatus: number): ComponentProps[" return "success" } -const getResourceLabel = (resource: AuditLog["resource"]): string => { - if ("name" in resource) { - return resource.name - } - - return resource.username -} - -const getResourceHref = ( - resource: AuditLog["resource"], - resourceType: AuditLog["resource_type"], -): string | undefined => { - switch (resourceType) { - case "user": - return `/users` - case "template": - return `/templates/${(resource as Template).name}` - case "workspace": - return `/workspaces/@${(resource as Workspace).owner_name}/${(resource as Workspace).name}` - case "organization": - return - } -} - -const ResourceLink: React.FC<{ - resource: AuditLog["resource"] - resourceType: AuditLog["resource_type"] -}> = ({ resource, resourceType }) => { - const href = getResourceHref(resource, resourceType) - const label = {getResourceLabel(resource)} - - if (!href) { - return label - } - - return ( - - {label} - - ) -} - const actionLabelByAction: Record = { create: t("actions.create", { ns: "auditLog" }), write: t("actions.write", { ns: "auditLog" }), @@ -140,10 +96,7 @@ export const AuditLogRow: React.FC = ({
{auditLog.user?.username} {readableActionMessage(auditLog)}{" "} - + {auditLog.resource_target} {createDayString(auditLog.time)}
From b7ccb0fc3294dd9fc938ef2d4784edf9c16888a2 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 31 Aug 2022 23:34:39 +0000 Subject: [PATCH 13/28] Fix types and minor issues on mobile --- codersdk/audit.go | 6 +++--- site/src/api/typesGenerated.ts | 6 +++--- site/src/components/AuditLogRow/AuditLogDiff.tsx | 8 +++----- site/src/components/AuditLogRow/AuditLogRow.tsx | 12 +++++++++++- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/codersdk/audit.go b/codersdk/audit.go index c9a7296cb104d..f3b4d1316299f 100644 --- a/codersdk/audit.go +++ b/codersdk/audit.go @@ -29,9 +29,9 @@ const ( type AuditDiff map[string]AuditDiffField type AuditDiffField struct { - Old any - New any - Secret bool + Old any `json:"old"` + New any `json:"new"` + Secret bool `json:"secret"` } type AuditLog struct { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 655787e0abb72..d7147f42eec4b 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -40,10 +40,10 @@ export type AuditDiff = Record // From codersdk/audit.go export interface AuditDiffField { // eslint-disable-next-line - readonly Old: any + readonly old: any // eslint-disable-next-line - readonly New: any - readonly Secret: boolean + readonly new: any + readonly secret: boolean } // From codersdk/audit.go diff --git a/site/src/components/AuditLogRow/AuditLogDiff.tsx b/site/src/components/AuditLogRow/AuditLogDiff.tsx index 3d48ff4c46dbb..905e4345b2628 100644 --- a/site/src/components/AuditLogRow/AuditLogDiff.tsx +++ b/site/src/components/AuditLogRow/AuditLogDiff.tsx @@ -22,7 +22,7 @@ export const AuditLogDiff: React.FC<{ diff: AuditLog["diff"] }> = ({ diff }) =>
{index + 1}
-
-
+
{attrName}:{" "} {getDiffValue(valueDiff.old)} @@ -36,7 +36,7 @@ export const AuditLogDiff: React.FC<{ diff: AuditLog["diff"] }> = ({ diff }) =>
{index + 1}
+
-
+
{attrName}:{" "} {getDiffValue(valueDiff.new)} @@ -76,9 +76,9 @@ const useStyles = makeStyles((theme) => ({ diffLine: { opacity: 0.5, - width: theme.spacing(8), textAlign: "right", + flexShrink: 0, }, diffIcon: { @@ -87,8 +87,6 @@ const useStyles = makeStyles((theme) => ({ fontSize: theme.typography.body1.fontSize, }, - diffContent: {}, - diffNew: { backgroundColor: theme.palette.success.dark, color: theme.palette.success.contrastText, diff --git a/site/src/components/AuditLogRow/AuditLogRow.tsx b/site/src/components/AuditLogRow/AuditLogRow.tsx index 3a6b0682aee59..7286add7d4e85 100644 --- a/site/src/components/AuditLogRow/AuditLogRow.tsx +++ b/site/src/components/AuditLogRow/AuditLogRow.tsx @@ -102,7 +102,12 @@ export const AuditLogRow: React.FC = ({
- + ({ display: "block", }, + auditLogRight: { + width: "auto", + }, + auditLogExtraInfo: { ...theme.typography.body2, fontFamily: "inherit", color: theme.palette.text.secondary, + whiteSpace: "nowrap", }, disabledDropdownIcon: { From 03eadb651d0a3368e39e73db8d0ca1236efa3399 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 1 Sep 2022 13:28:56 +0000 Subject: [PATCH 14/28] Handle errors --- site/src/xServices/audit/auditXService.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/site/src/xServices/audit/auditXService.ts b/site/src/xServices/audit/auditXService.ts index b610a9b9f8751..f65515442e7d9 100644 --- a/site/src/xServices/audit/auditXService.ts +++ b/site/src/xServices/audit/auditXService.ts @@ -1,4 +1,6 @@ import { getAuditLogs } from "api/api" +import { getErrorMessage } from "api/errors" +import { displayError } from "components/GlobalSnackbar/utils" import { assign, createMachine } from "xstate" type AuditLogs = Awaited> @@ -24,11 +26,18 @@ export const auditMachine = createMachine( target: "success", actions: ["assignAuditLogs"], }, + onError: { + target: "error", + actions: ["displayLoadAuditLogsError"], + }, }, }, success: { type: "final", }, + error: { + type: "final", + }, }, }, { @@ -36,6 +45,10 @@ export const auditMachine = createMachine( assignAuditLogs: assign({ auditLogs: (_, event) => event.data, }), + displayLoadAuditLogsError: (_, event) => { + const message = getErrorMessage(event.data, "Error on loading audit logs.") + displayError(message) + }, }, services: { loadAuditLogs: () => getAuditLogs(), From 77fa647a1a70e55659d46a5151a53d8c9867ed87 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 1 Sep 2022 14:46:29 +0000 Subject: [PATCH 15/28] Add pagination --- site/src/AppRouter.tsx | 24 ++--- site/src/api/api.ts | 14 ++- site/src/pages/AuditPage/AuditPage.tsx | 27 +++++- .../pages/AuditPage/AuditPageView.stories.tsx | 13 ++- site/src/pages/AuditPage/AuditPageView.tsx | 38 +++++++- site/src/testHelpers/entities.ts | 11 ++- site/src/theme/overrides.ts | 6 ++ site/src/theme/palettes.ts | 2 +- site/src/xServices/audit/auditXService.ts | 91 ++++++++++++++++--- 9 files changed, 183 insertions(+), 43 deletions(-) diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 5f2955eb3a6fa..5cbedf6d3350f 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -4,7 +4,7 @@ import { RequirePermission } from "components/RequirePermission/RequirePermissio import { SetupPage } from "pages/SetupPage/SetupPage" import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateSettingsPage" import { FC, lazy, Suspense, useContext } from "react" -import { Navigate, Route, Routes } from "react-router-dom" +import { Route, Routes } from "react-router-dom" import { selectPermissions } from "xServices/auth/authSelectors" import { selectFeatureVisibility } from "xServices/entitlements/entitlementsSelectors" import { XServiceContext } from "xServices/StateContext" @@ -139,19 +139,15 @@ export const AppRouter: FC = () => { - ) : ( - - - - - - ) + + + + + } > diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 9272d032266d9..ceae1aa308431 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -384,7 +384,17 @@ export const getEntitlements = async (): Promise => { return response.data } -export const getAuditLogs = async (): Promise => { - const response = await axios.get("/api/v2/audit") +interface GetAuditLogsOptions { + limit: number + offset: number +} + +export const getAuditLogs = async (options: GetAuditLogsOptions): Promise => { + const response = await axios.get(`/api/v2/audit?limit=${options}&offset=${options.offset}`) + return response.data +} + +export const getAuditLogsCount = async (): Promise => { + const response = await axios.get(`/api/v2/audit/count`) return response.data } diff --git a/site/src/pages/AuditPage/AuditPage.tsx b/site/src/pages/AuditPage/AuditPage.tsx index 2e4fb303549ca..8bf80f481d801 100644 --- a/site/src/pages/AuditPage/AuditPage.tsx +++ b/site/src/pages/AuditPage/AuditPage.tsx @@ -4,10 +4,31 @@ import { auditMachine } from "xServices/audit/auditXService" import { AuditPageView } from "./AuditPageView" const AuditPage: FC = () => { - const [auditState] = useMachine(auditMachine) - const { auditLogs } = auditState.context + const [auditState, auditSend] = useMachine(auditMachine, { + context: { + page: 1, + limit: 25, + }, + }) + const { auditLogs, count, page, limit } = auditState.context - return + return ( + { + auditSend("NEXT") + }} + onPrevious={() => { + auditSend("PREVIOUS") + }} + onGoToPage={(page) => { + auditSend("GO_TO_PAGE", { page }) + }} + /> + ) } export default AuditPage diff --git a/site/src/pages/AuditPage/AuditPageView.stories.tsx b/site/src/pages/AuditPage/AuditPageView.stories.tsx index 3687970e61800..f0275a6b9bae7 100644 --- a/site/src/pages/AuditPage/AuditPageView.stories.tsx +++ b/site/src/pages/AuditPage/AuditPageView.stories.tsx @@ -1,20 +1,29 @@ import { ComponentMeta, Story } from "@storybook/react" import { MockAuditLog, MockAuditLog2 } from "testHelpers/entities" -import { AuditPageView } from "./AuditPageView" +import { AuditPageView, AuditPageViewProps } from "./AuditPageView" export default { title: "pages/AuditPageView", component: AuditPageView, } as ComponentMeta -const Template: Story = (args) => +const Template: Story = (args) => export const AuditPage = Template.bind({}) AuditPage.args = { auditLogs: [MockAuditLog, MockAuditLog2], + count: 1000, + page: 1, + limit: 25, } export const AuditPageSmallViewport = Template.bind({}) +AuditPageSmallViewport.args = { + auditLogs: [MockAuditLog, MockAuditLog2], + count: 1000, + page: 1, + limit: 25, +} AuditPageSmallViewport.parameters = { chromatic: { viewports: [600] }, } diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index 66c4ec6cb5afb..99d92cbc72447 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -9,6 +9,7 @@ import { AuditLogRow } from "components/AuditLogRow/AuditLogRow" import { CodeExample } from "components/CodeExample/CodeExample" import { Margins } from "components/Margins/Margins" import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "components/PageHeader/PageHeader" +import { PaginationWidget } from "components/PaginationWidget/PaginationWidget" import { Stack } from "components/Stack/Stack" import { TableLoader } from "components/TableLoader/TableLoader" import { AuditHelpTooltip } from "components/Tooltips" @@ -20,7 +21,27 @@ export const Language = { tooltipTitle: "Copy to clipboard and try the Coder CLI", } -export const AuditPageView: FC<{ auditLogs?: AuditLog[] }> = ({ auditLogs }) => { +export interface AuditPageViewProps { + auditLogs?: AuditLog[] + count?: number + page: number + limit: number + onNext: () => void + onPrevious: () => void + onGoToPage: (page: number) => void +} + +export const AuditPageView: FC = ({ + auditLogs, + count, + page, + limit, + onNext, + onPrevious, + onGoToPage, +}) => { + const isReady = auditLogs && count + return ( = ({ auditLogs }) => - {auditLogs ? ( + {isReady ? ( auditLogs.map((auditLog) => ) ) : ( @@ -53,6 +74,19 @@ export const AuditPageView: FC<{ auditLogs?: AuditLog[] }> = ({ auditLogs }) => + + {isReady && count > limit && ( + + )} ) } diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index bf2abc254e1cc..3d48d17b6f0ac 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -704,14 +704,14 @@ export const MockAuditLog: TypesGen.AuditLog = { user_agent: "browser", resource_type: "workspace", resource_id: "ef8d1cf4-82de-4fd9-8980-047dad6d06b5", - resource_target: "Bruno's Org", + resource_target: "bruno-dev", + resource_icon: "", action: "create", diff: {}, status_code: 200, - additional_fields: {}, - description: "Colin Adler updated the organization Bruno's Org", + additional_fields: "", + description: "Colin Adler updated the workspace bruno-dev", user: MockUser, - resource: MockWorkspace, } export const MockAuditLog2: TypesGen.AuditLog = { @@ -722,14 +722,17 @@ export const MockAuditLog2: TypesGen.AuditLog = { workspace_name: { old: "old-workspace-name", new: MockWorkspace.name, + secret: false, }, workspace_auto_off: { old: true, new: false, + secret: false, }, template_version_id: { old: "fbd2116a-8961-4954-87ae-e4575bd29ce0", new: "53bded77-7b9d-4e82-8771-991a34d759f9", + secret: false, }, }, } diff --git a/site/src/theme/overrides.ts b/site/src/theme/overrides.ts index 45a474fce761b..299c34e7dc3d6 100644 --- a/site/src/theme/overrides.ts +++ b/site/src/theme/overrides.ts @@ -41,10 +41,16 @@ export const getOverrides = ({ palette, breakpoints }: Theme): Overrides => { boxShadow: "none", color: palette.text.primary, backgroundColor: colors.gray[17], + "&:hover": { boxShadow: "none", backgroundColor: "#000000", }, + + "&.Mui-disabled": { + backgroundColor: palette.background.paper, + color: palette.secondary.main, + }, }, sizeSmall: { padding: `0 12px`, diff --git a/site/src/theme/palettes.ts b/site/src/theme/palettes.ts index eb2fd91294893..d281b15cdb167 100644 --- a/site/src/theme/palettes.ts +++ b/site/src/theme/palettes.ts @@ -13,7 +13,7 @@ export const darkPalette: PaletteOptions = { dark: colors.blue[9], }, secondary: { - main: colors.green[11], + main: colors.gray[11], contrastText: colors.gray[3], dark: colors.indigo[7], }, diff --git a/site/src/xServices/audit/auditXService.ts b/site/src/xServices/audit/auditXService.ts index f65515442e7d9..df503bef9a010 100644 --- a/site/src/xServices/audit/auditXService.ts +++ b/site/src/xServices/audit/auditXService.ts @@ -1,4 +1,4 @@ -import { getAuditLogs } from "api/api" +import { getAuditLogs, getAuditLogsCount } from "api/api" import { getErrorMessage } from "api/errors" import { displayError } from "components/GlobalSnackbar/utils" import { assign, createMachine } from "xstate" @@ -9,31 +9,70 @@ export const auditMachine = createMachine( { id: "auditMachine", schema: { - context: {} as { auditLogs?: AuditLogs }, + context: {} as { auditLogs?: AuditLogs; count?: number; page: number; limit: number }, services: {} as { loadAuditLogs: { data: AuditLogs } + loadAuditLogsCount: { + data: number + } }, + events: {} as + | { + type: "NEXT" + } + | { + type: "PREVIOUS" + } + | { + type: "GO_TO_PAGE" + page: number + }, }, tsTypes: {} as import("./auditXService.typegen").Typegen0, - initial: "loadingLogs", + initial: "loading", states: { - loadingLogs: { - invoke: { - src: "loadAuditLogs", - onDone: { - target: "success", - actions: ["assignAuditLogs"], + loading: { + invoke: [ + { + src: "loadAuditLogs", + onDone: { + actions: ["assignAuditLogs"], + }, + onError: { + target: "error", + actions: ["displayLoadAuditLogsError"], + }, }, - onError: { - target: "error", - actions: ["displayLoadAuditLogsError"], + { + src: "loadAuditLogsCount", + onDone: { + actions: ["assignCount"], + }, + onError: { + target: "error", + actions: ["displayLoadAuditLogsCountError"], + }, }, - }, + ], + onDone: "success", }, success: { - type: "final", + on: { + NEXT: { + actions: ["assignNextPage"], + target: "loading", + }, + PREVIOUS: { + actions: ["assignPreviousPage"], + target: "loading", + }, + GO_TO_PAGE: { + actions: ["assignPage"], + target: "loading", + }, + }, }, error: { type: "final", @@ -45,13 +84,35 @@ export const auditMachine = createMachine( assignAuditLogs: assign({ auditLogs: (_, event) => event.data, }), + assignCount: assign({ + count: (_, event) => event.data, + }), + assignNextPage: assign({ + page: ({ page }) => page + 1, + }), + assignPreviousPage: assign({ + page: ({ page }) => page - 1, + }), + assignPage: assign({ + page: ({ page }) => page, + }), displayLoadAuditLogsError: (_, event) => { const message = getErrorMessage(event.data, "Error on loading audit logs.") displayError(message) }, + displayLoadAuditLogsCountError: (_, event) => { + const message = getErrorMessage(event.data, "Error on loading audit logs count.") + displayError(message) + }, }, services: { - loadAuditLogs: () => getAuditLogs(), + loadAuditLogs: ({ page, limit }, _) => + getAuditLogs({ + // The page in the API starts at 0 + offset: (page - 1) * limit, + limit, + }), + loadAuditLogsCount: () => getAuditLogsCount(), }, }, ) From dc1543c39e92d617d115dd894f36ee7e6bc6b5aa Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 1 Sep 2022 15:00:45 +0000 Subject: [PATCH 16/28] Fix table alignment --- site/src/components/AuditLogRow/AuditLogRow.stories.tsx | 2 +- site/src/pages/AuditPage/AuditPageView.tsx | 2 +- site/src/theme/overrides.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/site/src/components/AuditLogRow/AuditLogRow.stories.tsx b/site/src/components/AuditLogRow/AuditLogRow.stories.tsx index bb6c03b083126..b552b868ac240 100644 --- a/site/src/components/AuditLogRow/AuditLogRow.stories.tsx +++ b/site/src/components/AuditLogRow/AuditLogRow.stories.tsx @@ -18,7 +18,7 @@ const Template: Story = (args) => ( - Logs + Logs diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index 99d92cbc72447..3c831921c7de1 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -62,7 +62,7 @@ export const AuditPageView: FC = ({
- Logs + Logs diff --git a/site/src/theme/overrides.ts b/site/src/theme/overrides.ts index 299c34e7dc3d6..835d531952525 100644 --- a/site/src/theme/overrides.ts +++ b/site/src/theme/overrides.ts @@ -126,10 +126,10 @@ export const getOverrides = ({ palette, breakpoints }: Theme): Overrides => { padding: "12px 8px", // This targets the first+last td elements, and also the first+last elements // of a TableCellLink. - "&:first-child, &:first-child > a": { + "&:not(:only-child):first-child, &:not(:only-child):first-child > a": { paddingLeft: 32, }, - "&:last-child, &:last-child > a": { + "&:not(:only-child):last-child, &:not(:only-child):last-child > a": { paddingRight: 32, }, }, From 901c5f27f81eb6c1c19ce25f22e0dd68faf76eb1 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 1 Sep 2022 15:09:26 +0000 Subject: [PATCH 17/28] Add handler for /count --- site/src/api/api.ts | 2 +- site/src/testHelpers/handlers.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ceae1aa308431..944412cbbad6b 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -390,7 +390,7 @@ interface GetAuditLogsOptions { } export const getAuditLogs = async (options: GetAuditLogsOptions): Promise => { - const response = await axios.get(`/api/v2/audit?limit=${options}&offset=${options.offset}`) + const response = await axios.get(`/api/v2/audit?limit=${options.limit}&offset=${options.offset}`) return response.data } diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 5c76aa4834f4a..95b6d6477b233 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -152,4 +152,8 @@ export const handlers = [ rest.get("/api/v2/audit", (req, res, ctx) => { return res(ctx.status(200), ctx.json([M.MockAuditLog, M.MockAuditLog2])) }), + + rest.get("/api/v2/audit/count", (req, res, ctx) => { + return res(ctx.status(200), ctx.json(1000)) + }), ] From ebe2792369f073dbecf7364bccce6c16a0529ac8 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Thu, 1 Sep 2022 16:57:49 -0300 Subject: [PATCH 18/28] Update site/src/xServices/audit/auditXService.ts Co-authored-by: Presley Pizzo <1290996+presleyp@users.noreply.github.com> --- site/src/xServices/audit/auditXService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/xServices/audit/auditXService.ts b/site/src/xServices/audit/auditXService.ts index df503bef9a010..9fe6afdcd9c32 100644 --- a/site/src/xServices/audit/auditXService.ts +++ b/site/src/xServices/audit/auditXService.ts @@ -101,7 +101,7 @@ export const auditMachine = createMachine( displayError(message) }, displayLoadAuditLogsCountError: (_, event) => { - const message = getErrorMessage(event.data, "Error on loading audit logs count.") + const message = getErrorMessage(event.data, "Error on loading number of audit log entries.") displayError(message) }, }, From cfe9d7d1c117aca62ba13047afd142e9291275dd Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 7 Sep 2022 16:50:56 +0000 Subject: [PATCH 19/28] Fix types --- site/src/api/api.ts | 6 ++++-- site/src/xServices/audit/auditXService.ts | 11 +++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 6e5ece1505d5a..a285248c309ea 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -394,12 +394,14 @@ interface GetAuditLogsOptions { offset: number } -export const getAuditLogs = async (options: GetAuditLogsOptions): Promise => { +export const getAuditLogs = async ( + options: GetAuditLogsOptions, +): Promise => { const response = await axios.get(`/api/v2/audit?limit=${options.limit}&offset=${options.offset}`) return response.data } -export const getAuditLogsCount = async (): Promise => { +export const getAuditLogsCount = async (): Promise => { const response = await axios.get(`/api/v2/audit/count`) return response.data } diff --git a/site/src/xServices/audit/auditXService.ts b/site/src/xServices/audit/auditXService.ts index df503bef9a010..9c2cc321b4ea7 100644 --- a/site/src/xServices/audit/auditXService.ts +++ b/site/src/xServices/audit/auditXService.ts @@ -1,18 +1,17 @@ import { getAuditLogs, getAuditLogsCount } from "api/api" import { getErrorMessage } from "api/errors" +import { AuditLog } from "api/typesGenerated" import { displayError } from "components/GlobalSnackbar/utils" import { assign, createMachine } from "xstate" -type AuditLogs = Awaited> - export const auditMachine = createMachine( { id: "auditMachine", schema: { - context: {} as { auditLogs?: AuditLogs; count?: number; page: number; limit: number }, + context: {} as { auditLogs?: AuditLog[]; count?: number; page: number; limit: number }, services: {} as { loadAuditLogs: { - data: AuditLogs + data: AuditLog[] } loadAuditLogsCount: { data: number @@ -111,8 +110,8 @@ export const auditMachine = createMachine( // The page in the API starts at 0 offset: (page - 1) * limit, limit, - }), - loadAuditLogsCount: () => getAuditLogsCount(), + }).then((data) => data.audit_logs), + loadAuditLogsCount: () => getAuditLogsCount().then((data) => data.count), }, }, ) From f1ba4acda9d4e51388e07352fa0e8422479d6d7d Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 7 Sep 2022 17:10:00 +0000 Subject: [PATCH 20/28] Fix empty state --- .../components/AuditLogRow/AuditLogRow.tsx | 5 ++- site/src/components/UserAvatar/UserAvatar.tsx | 4 +- site/src/pages/AuditPage/AuditPage.tsx | 37 +++++++++++-------- site/src/pages/AuditPage/AuditPageView.tsx | 22 +++++++---- 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/site/src/components/AuditLogRow/AuditLogRow.tsx b/site/src/components/AuditLogRow/AuditLogRow.tsx index 7286add7d4e85..58efbe6c5354e 100644 --- a/site/src/components/AuditLogRow/AuditLogRow.tsx +++ b/site/src/components/AuditLogRow/AuditLogRow.tsx @@ -92,7 +92,10 @@ export const AuditLogRow: React.FC = ({ className={styles.auditLogRowInfo} > - +
{auditLog.user?.username} {readableActionMessage(auditLog)}{" "} diff --git a/site/src/components/UserAvatar/UserAvatar.tsx b/site/src/components/UserAvatar/UserAvatar.tsx index d03041ddeca76..c861a983efa34 100644 --- a/site/src/components/UserAvatar/UserAvatar.tsx +++ b/site/src/components/UserAvatar/UserAvatar.tsx @@ -3,9 +3,9 @@ import { FC } from "react" import { firstLetter } from "../../util/firstLetter" export interface UserAvatarProps { - className?: string username: string - avatarURL: string + className?: string + avatarURL?: string } export const UserAvatar: FC = ({ username, className, avatarURL }) => { diff --git a/site/src/pages/AuditPage/AuditPage.tsx b/site/src/pages/AuditPage/AuditPage.tsx index 8bf80f481d801..29b54a11356cb 100644 --- a/site/src/pages/AuditPage/AuditPage.tsx +++ b/site/src/pages/AuditPage/AuditPage.tsx @@ -1,5 +1,7 @@ import { useMachine } from "@xstate/react" import { FC } from "react" +import { Helmet } from "react-helmet-async" +import { pageTitle } from "util/page" import { auditMachine } from "xServices/audit/auditXService" import { AuditPageView } from "./AuditPageView" @@ -13,21 +15,26 @@ const AuditPage: FC = () => { const { auditLogs, count, page, limit } = auditState.context return ( - { - auditSend("NEXT") - }} - onPrevious={() => { - auditSend("PREVIOUS") - }} - onGoToPage={(page) => { - auditSend("GO_TO_PAGE", { page }) - }} - /> + <> + + {pageTitle("Audit")} + + { + auditSend("NEXT") + }} + onPrevious={() => { + auditSend("PREVIOUS") + }} + onGoToPage={(page) => { + auditSend("GO_TO_PAGE", { page }) + }} + /> + ) } diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index 3c831921c7de1..0b0de00d587a7 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -7,6 +7,7 @@ import TableRow from "@material-ui/core/TableRow" import { AuditLog } from "api/typesGenerated" import { AuditLogRow } from "components/AuditLogRow/AuditLogRow" import { CodeExample } from "components/CodeExample/CodeExample" +import { EmptyState } from "components/EmptyState/EmptyState" import { Margins } from "components/Margins/Margins" import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "components/PageHeader/PageHeader" import { PaginationWidget } from "components/PaginationWidget/PaginationWidget" @@ -40,7 +41,9 @@ export const AuditPageView: FC = ({ onPrevious, onGoToPage, }) => { - const isReady = auditLogs && count + const isLoading = auditLogs === undefined || count === undefined + const isEmpty = !isLoading && auditLogs.length === 0 + const hasResults = !isLoading && auditLogs.length > 0 return ( @@ -66,16 +69,21 @@ export const AuditPageView: FC = ({ - {isReady ? ( - auditLogs.map((auditLog) => ) - ) : ( - + {isLoading && } + {hasResults && + auditLogs.map((auditLog) => )} + {isEmpty && ( + + + + + )}
- {isReady && count > limit && ( + {!isLoading && count > limit ? ( = ({ activePage={page} numRecordsPerPage={limit} /> - )} + ) : null} ) } From 8f1bce73182671d46145c610641b31d63d1e855e Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 7 Sep 2022 17:36:44 +0000 Subject: [PATCH 21/28] Display user agent info --- site/package.json | 2 ++ .../components/AuditLogRow/AuditLogDiff.tsx | 2 ++ .../components/AuditLogRow/AuditLogRow.tsx | 21 ++++++++++++------- site/yarn.lock | 10 +++++++++ 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/site/package.json b/site/package.json index 70ed8cca3d33c..d79a49cb81fb2 100644 --- a/site/package.json +++ b/site/package.json @@ -60,6 +60,7 @@ "sourcemapped-stacktrace": "1.1.11", "swr": "1.3.0", "tzdata": "1.0.30", + "ua-parser-js": "1.0.2", "uuid": "9.0.0", "xstate": "4.33.5", "xterm": "4.19.0", @@ -87,6 +88,7 @@ "@types/react-helmet": "6.1.5", "@types/semver": "^7.3.12", "@types/superagent": "4.1.15", + "@types/ua-parser-js": "^0.7.36", "@types/uuid": "8.3.4", "@typescript-eslint/eslint-plugin": "5.36.1", "@typescript-eslint/parser": "5.31.0", diff --git a/site/src/components/AuditLogRow/AuditLogDiff.tsx b/site/src/components/AuditLogRow/AuditLogDiff.tsx index 905e4345b2628..6dffe114018bd 100644 --- a/site/src/components/AuditLogRow/AuditLogDiff.tsx +++ b/site/src/components/AuditLogRow/AuditLogDiff.tsx @@ -1,6 +1,7 @@ import { makeStyles } from "@material-ui/core/styles" import { AuditLog } from "api/typesGenerated" import { colors } from "theme/colors" +import { MONOSPACE_FONT_FAMILY } from "theme/constants" import { combineClasses } from "util/combineClasses" const getDiffValue = (value: number | string | boolean) => { @@ -55,6 +56,7 @@ const useStyles = makeStyles((theme) => ({ alignItems: "flex-start", fontSize: theme.typography.body2.fontSize, borderTop: `1px solid ${theme.palette.divider}`, + fontFamily: MONOSPACE_FONT_FAMILY, }, diffColumn: { diff --git a/site/src/components/AuditLogRow/AuditLogRow.tsx b/site/src/components/AuditLogRow/AuditLogRow.tsx index 58efbe6c5354e..50a4a9c890647 100644 --- a/site/src/components/AuditLogRow/AuditLogRow.tsx +++ b/site/src/components/AuditLogRow/AuditLogRow.tsx @@ -9,8 +9,10 @@ import { Stack } from "components/Stack/Stack" import { UserAvatar } from "components/UserAvatar/UserAvatar" import { t } from "i18next" import { ComponentProps, useState } from "react" +import { MONOSPACE_FONT_FAMILY } from "theme/constants" import { createDayString } from "util/createDayString" import { AuditLogDiff } from "./AuditLogDiff" +import userAgentParser from "ua-parser-js" const pillTypeByHttpStatus = (httpStatus: number): ComponentProps["type"] => { if (httpStatus >= 300 && httpStatus < 500) { @@ -58,6 +60,7 @@ export const AuditLogRow: React.FC = ({ const [isDiffOpen, setIsDiffOpen] = useState(defaultIsDiffOpen) const diffs = Object.entries(auditLog.diff) const shouldDisplayDiff = diffs.length > 0 + const userAgent = userAgentParser(auditLog.user_agent) const toggle = () => { if (shouldDisplayDiff) { @@ -66,11 +69,7 @@ export const AuditLogRow: React.FC = ({ } return ( - + = ({ IP {auditLog.ip}
- Agent {auditLog.user_agent} + OS {userAgent.os.name} +
+
+ Browser {userAgent.browser.name} {userAgent.browser.version}
@@ -148,6 +150,10 @@ const useStyles = makeStyles((theme) => ({ auditLogRow: { padding: theme.spacing(2, 4), + + "&:hover": { + backgroundColor: theme.palette.action.hover, + }, }, auditLogRowInfo: { @@ -162,6 +168,7 @@ const useStyles = makeStyles((theme) => ({ auditLogTime: { ...theme.typography.body2, + fontSize: 12, fontFamily: "inherit", color: theme.palette.text.secondary, display: "block", @@ -173,7 +180,7 @@ const useStyles = makeStyles((theme) => ({ auditLogExtraInfo: { ...theme.typography.body2, - fontFamily: "inherit", + fontFamily: MONOSPACE_FONT_FAMILY, color: theme.palette.text.secondary, whiteSpace: "nowrap", }, diff --git a/site/yarn.lock b/site/yarn.lock index 8af63ea04f0fc..f8e9c9751c857 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -3671,6 +3671,11 @@ dependencies: "@types/jest" "*" +"@types/ua-parser-js@^0.7.36": + version "0.7.36" + resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190" + integrity sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ== + "@types/uglify-js@*": version "3.17.0" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.17.0.tgz#95271e7abe0bf7094c60284f76ee43232aef43b9" @@ -14246,6 +14251,11 @@ tzdata@1.0.30: resolved "https://registry.yarnpkg.com/tzdata/-/tzdata-1.0.30.tgz#d9d5a4b4b5e1ed95f6255f98c0564c4256316f52" integrity sha512-/0yogZsIRUVhGIEGZahL+Nnl9gpMD6jtQ9MlVtPVofFwhaqa+cFTgRy1desTAKqdmIJjS6CL+i6F/mnetrLaxw== +ua-parser-js@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.2.tgz#e2976c34dbfb30b15d2c300b2a53eac87c57a775" + integrity sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg== + uglify-js@^3.1.4: version "3.17.0" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.0.tgz#55bd6e9d19ce5eef0d5ad17cd1f587d85b180a85" From b72c3a96b8f05a26ca1f48e754a8e1b6ce3ce90f Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 7 Sep 2022 18:32:31 +0000 Subject: [PATCH 22/28] Fix audit log pagination --- site/src/pages/AuditPage/AuditPage.tsx | 14 +++- site/src/pages/AuditPage/AuditPageView.tsx | 2 +- site/src/xServices/audit/auditXService.ts | 88 +++++++++++----------- 3 files changed, 57 insertions(+), 47 deletions(-) diff --git a/site/src/pages/AuditPage/AuditPage.tsx b/site/src/pages/AuditPage/AuditPage.tsx index 29b54a11356cb..fca6fcc7556a9 100644 --- a/site/src/pages/AuditPage/AuditPage.tsx +++ b/site/src/pages/AuditPage/AuditPage.tsx @@ -1,16 +1,27 @@ import { useMachine } from "@xstate/react" import { FC } from "react" import { Helmet } from "react-helmet-async" +import { useNavigate, useSearchParams } from "react-router-dom" import { pageTitle } from "util/page" import { auditMachine } from "xServices/audit/auditXService" import { AuditPageView } from "./AuditPageView" const AuditPage: FC = () => { + const navigate = useNavigate() + const [searchParams] = useSearchParams() + const currentPage = searchParams.get("page") ? Number(searchParams.get("page")) : 1 const [auditState, auditSend] = useMachine(auditMachine, { context: { - page: 1, + page: currentPage, limit: 25, }, + actions: { + onPageChange: ({ page }) => { + navigate({ + search: `?page=${page}`, + }) + }, + }, }) const { auditLogs, count, page, limit } = auditState.context @@ -31,6 +42,7 @@ const AuditPage: FC = () => { auditSend("PREVIOUS") }} onGoToPage={(page) => { + console.log("PAGE", page) auditSend("GO_TO_PAGE", { page }) }} /> diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index 0b0de00d587a7..526b9890407ce 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -83,7 +83,7 @@ export const AuditPageView: FC = ({ - {!isLoading && count > limit ? ( + {count && count > limit ? ( event.data, + clearPreviousAuditLogs: assign({ + auditLogs: (_) => undefined, }), - assignCount: assign({ - count: (_, event) => event.data, + assignAuditLogsAndCount: assign({ + auditLogs: (_, event) => event.data.auditLogs, + count: (_, event) => event.data.count, }), assignNextPage: assign({ page: ({ page }) => page + 1, @@ -93,25 +87,29 @@ export const auditMachine = createMachine( page: ({ page }) => page - 1, }), assignPage: assign({ - page: ({ page }) => page, + page: (_, { page }) => page, }), - displayLoadAuditLogsError: (_, event) => { + displayApiError: (_, event) => { const message = getErrorMessage(event.data, "Error on loading audit logs.") displayError(message) }, - displayLoadAuditLogsCountError: (_, event) => { - const message = getErrorMessage(event.data, "Error on loading number of audit log entries.") - displayError(message) - }, }, services: { - loadAuditLogs: ({ page, limit }, _) => - getAuditLogs({ - // The page in the API starts at 0 - offset: (page - 1) * limit, - limit, - }).then((data) => data.audit_logs), - loadAuditLogsCount: () => getAuditLogsCount().then((data) => data.count), + loadAuditLogsAndCount: async ({ page, limit }, _) => { + const [auditLogs, count] = await Promise.all([ + getAuditLogs({ + // The page in the API starts at 0 + offset: (page - 1) * limit, + limit, + }).then((data) => data.audit_logs), + getAuditLogsCount().then((data) => data.count), + ]) + + return { + auditLogs, + count, + } + }, }, }, ) From 0e052ae730a2f54cba2e40e0bc96a20a57672008 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 7 Sep 2022 18:33:52 +0000 Subject: [PATCH 23/28] Remove console log --- site/src/pages/AuditPage/AuditPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/site/src/pages/AuditPage/AuditPage.tsx b/site/src/pages/AuditPage/AuditPage.tsx index fca6fcc7556a9..342b0fbc8dc8a 100644 --- a/site/src/pages/AuditPage/AuditPage.tsx +++ b/site/src/pages/AuditPage/AuditPage.tsx @@ -42,7 +42,6 @@ const AuditPage: FC = () => { auditSend("PREVIOUS") }} onGoToPage={(page) => { - console.log("PAGE", page) auditSend("GO_TO_PAGE", { page }) }} /> From acaa45a3a416285b9ce0c9401db7b21dba8a644b Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 7 Sep 2022 18:43:14 +0000 Subject: [PATCH 24/28] Fix handlers and format --- site/src/components/AuditLogRow/AuditLogRow.tsx | 2 +- site/src/testHelpers/handlers.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/site/src/components/AuditLogRow/AuditLogRow.tsx b/site/src/components/AuditLogRow/AuditLogRow.tsx index 50a4a9c890647..1974956c05303 100644 --- a/site/src/components/AuditLogRow/AuditLogRow.tsx +++ b/site/src/components/AuditLogRow/AuditLogRow.tsx @@ -10,9 +10,9 @@ import { UserAvatar } from "components/UserAvatar/UserAvatar" import { t } from "i18next" import { ComponentProps, useState } from "react" import { MONOSPACE_FONT_FAMILY } from "theme/constants" +import userAgentParser from "ua-parser-js" import { createDayString } from "util/createDayString" import { AuditLogDiff } from "./AuditLogDiff" -import userAgentParser from "ua-parser-js" const pillTypeByHttpStatus = (httpStatus: number): ComponentProps["type"] => { if (httpStatus >= 300 && httpStatus < 500) { diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 1a7070a659f93..3c3b238dec1c6 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -157,10 +157,15 @@ export const handlers = [ // Audit rest.get("/api/v2/audit", (req, res, ctx) => { - return res(ctx.status(200), ctx.json([M.MockAuditLog, M.MockAuditLog2])) + return res( + ctx.status(200), + ctx.json({ + audit_logs: [M.MockAuditLog, M.MockAuditLog2], + }), + ) }), rest.get("/api/v2/audit/count", (req, res, ctx) => { - return res(ctx.status(200), ctx.json(1000)) + return res(ctx.status(200), ctx.json({ count: 1000 })) }), ] From 168cb16455dfb52ed17c5ff2f1a752ee274d4a8b Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 7 Sep 2022 18:47:01 +0000 Subject: [PATCH 25/28] Remove one test --- site/src/pages/AuditPage/AuditPage.test.tsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/site/src/pages/AuditPage/AuditPage.test.tsx b/site/src/pages/AuditPage/AuditPage.test.tsx index ff2f934cbd091..4ddfb5119cad8 100644 --- a/site/src/pages/AuditPage/AuditPage.test.tsx +++ b/site/src/pages/AuditPage/AuditPage.test.tsx @@ -1,5 +1,4 @@ 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 { MockAuditLog, MockAuditLog2, render } from "testHelpers/renderHelpers" @@ -26,17 +25,6 @@ describe("AuditPage", () => { expect(await screen.findByText(AuditTooltipLanguage.title)).toBeInTheDocument() }) - it("describes the CLI command", async () => { - // When - render() - - // 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() - }) - it("shows the audit logs", async () => { // When render() From c48a72ee9e057afd05b48aa79a987e0b7eb31ad4 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 7 Sep 2022 18:48:33 +0000 Subject: [PATCH 26/28] Update user agent on mocks --- site/src/testHelpers/entities.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 5ad4fdceafd77..28dd4372ae9a6 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -767,7 +767,8 @@ export const MockAuditLog: TypesGen.AuditLog = { time: "2022-05-19T16:45:57.122Z", organization_id: "fc0774ce-cc9e-48d4-80ae-88f7a4d4a8b0", ip: "127.0.0.1", - user_agent: "browser", + user_agent: + '"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36"', resource_type: "workspace", resource_id: "ef8d1cf4-82de-4fd9-8980-047dad6d06b5", resource_target: "bruno-dev", From c398be202aa3c42e4f82f24931ba602658b112aa Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 7 Sep 2022 19:06:10 +0000 Subject: [PATCH 27/28] Fix switch color on workspace schedule form --- .../components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx index ee439cbefec75..1b53583ed141c 100644 --- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx +++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx @@ -230,6 +230,7 @@ export const WorkspaceScheduleForm: FC } label={Language.startSwitch} From 6d09dfa376486935a14559462a1b0dd696bda4bf Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 7 Sep 2022 19:10:55 +0000 Subject: [PATCH 28/28] Fix button --- .../WorkspaceScheduleBanner/WorkspaceScheduleBanner.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/site/src/components/WorkspaceScheduleBanner/WorkspaceScheduleBanner.tsx b/site/src/components/WorkspaceScheduleBanner/WorkspaceScheduleBanner.tsx index a266a70b2b3f2..81538af5352df 100644 --- a/site/src/components/WorkspaceScheduleBanner/WorkspaceScheduleBanner.tsx +++ b/site/src/components/WorkspaceScheduleBanner/WorkspaceScheduleBanner.tsx @@ -42,7 +42,13 @@ export const WorkspaceScheduleBanner: FC + }