-
Notifications
You must be signed in to change notification settings - Fork 899
chore: Add Audit Log components and service to load from the API #3782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
b8fa378
38955a8
785f67b
7479538
b30165a
f9a7ed7
9db3385
294dceb
1b0ed59
6cf89a4
a5da88b
0267d4e
a0ce84e
b7ccb0f
03eadb6
77fa647
dc1543c
901c5f2
ebe2792
75f19b5
cfe9d7d
f1ba4ac
8f1bce7
b2fd91b
b72c3a9
0e052ae
acaa45a
168cb16
c48a72e
c398be2
6d09dfa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<div className={styles.diff}> | ||
<div className={combineClasses([styles.diffColumn, styles.diffOld])}> | ||
{diffEntries.map(([attrName, valueDiff], index) => ( | ||
<div key={attrName} className={styles.diffRow}> | ||
<div className={styles.diffLine}>{index + 1}</div> | ||
<div className={styles.diffIcon}>-</div> | ||
<div className={styles.diffContent}> | ||
{attrName}:{" "} | ||
<span className={combineClasses([styles.diffValue, styles.diffValueOld])}> | ||
{getDiffValue(valueDiff.old)} | ||
</span> | ||
</div> | ||
</div> | ||
))} | ||
</div> | ||
<div className={combineClasses([styles.diffColumn, styles.diffNew])}> | ||
{diffEntries.map(([attrName, valueDiff], index) => ( | ||
<div key={attrName} className={styles.diffRow}> | ||
<div className={styles.diffLine}>{index + 1}</div> | ||
<div className={styles.diffIcon}>+</div> | ||
<div className={styles.diffContent}> | ||
{attrName}:{" "} | ||
<span className={combineClasses([styles.diffValue, styles.diffValueNew])}> | ||
{getDiffValue(valueDiff.new)} | ||
</span> | ||
</div> | ||
</div> | ||
))} | ||
</div> | ||
</div> | ||
) | ||
} | ||
|
||
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], | ||
}, | ||
})) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof AuditLogRow> | ||
|
||
const Template: Story<AuditLogRowProps> = (args) => ( | ||
<TableContainer> | ||
<Table> | ||
<TableHead> | ||
<TableRow> | ||
<TableCell>Logs</TableCell> | ||
</TableRow> | ||
</TableHead> | ||
<TableBody> | ||
<AuditLogRow {...args} /> | ||
</TableBody> | ||
</Table> | ||
</TableContainer> | ||
) | ||
|
||
export const NoDiff = Template.bind({}) | ||
NoDiff.args = { | ||
auditLog: MockAuditLog, | ||
} | ||
|
||
export const WithDiff = Template.bind({}) | ||
WithDiff.args = { | ||
auditLog: MockAuditLog2, | ||
defaultIsDiffOpen: true, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 => { | ||
BrunoQuaresma marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 = <strong>{getResourceLabel(resource)}</strong> | ||
|
||
if (!href) { | ||
return label | ||
} | ||
|
||
return ( | ||
<Link component={RouterLink} to={href}> | ||
{label} | ||
</Link> | ||
) | ||
} | ||
|
||
const actionLabelByAction: Record<AuditLog["action"], string> = { | ||
create: "created a new", | ||
write: "updated", | ||
delete: "deleted", | ||
} | ||
BrunoQuaresma marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
const resourceLabelByResourceType: Record<AuditLog["resource_type"], string> = { | ||
organization: "organization", | ||
template: "template", | ||
template_version: "template version", | ||
user: "user", | ||
workspace: "workspace", | ||
} | ||
|
||
const readableActionMessage = (auditLog: AuditLog) => { | ||
return `${actionLabelByAction[auditLog.action]} ${ | ||
resourceLabelByResourceType[auditLog.resource_type] | ||
}` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's how to do interpolation with react-i18n https://www.i18next.com/translation-function/interpolation There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are planning to move this "message builder" to the BE so the CLI and FE can use the same language so I'm going just let this as it is but thanks for sharing the docs. Do you know how they could support interpolation with components? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the closest I've found https://www.i18next.com/translation-function/formatting |
||
} | ||
|
||
export interface AuditLogRowProps { | ||
auditLog: AuditLog | ||
// Useful for Storybook | ||
defaultIsDiffOpen?: boolean | ||
} | ||
|
||
export const AuditLogRow: React.FC<AuditLogRowProps> = ({ | ||
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 ( | ||
<TableRow key={auditLog.id} hover={shouldDisplayDiff}> | ||
<TableCell className={styles.auditLogCell}> | ||
<Stack | ||
style={{ cursor: shouldDisplayDiff ? "pointer" : undefined }} | ||
direction="row" | ||
alignItems="center" | ||
className={styles.auditLogRow} | ||
tabIndex={0} | ||
onClick={toggle} | ||
onKeyDown={(event) => { | ||
if (event.key === "Enter") { | ||
toggle() | ||
} | ||
}} | ||
> | ||
<Stack | ||
direction="row" | ||
alignItems="center" | ||
justifyContent="space-between" | ||
className={styles.auditLogRowInfo} | ||
> | ||
<Stack direction="row" alignItems="center"> | ||
<UserAvatar username={auditLog.user?.username ?? ""} /> | ||
<div> | ||
<span className={styles.auditLogResume}> | ||
<strong>{auditLog.user?.username}</strong> {readableActionMessage(auditLog)}{" "} | ||
<ResourceLink | ||
resource={auditLog.resource} | ||
resourceType={auditLog.resource_type} | ||
/> | ||
</span> | ||
<span className={styles.auditLogTime}>{createDayString(auditLog.time)}</span> | ||
</div> | ||
</Stack> | ||
|
||
<Stack direction="column" alignItems="flex-end" spacing={1}> | ||
<Pill type="success" text={auditLog.status_code.toString()} /> | ||
<Stack direction="row" alignItems="center" className={styles.auditLogExtraInfo}> | ||
<div> | ||
<strong>IP</strong> {auditLog.ip} | ||
</div> | ||
<div> | ||
<strong>Agent</strong> {auditLog.user_agent} | ||
</div> | ||
</Stack> | ||
</Stack> | ||
</Stack> | ||
|
||
<div className={shouldDisplayDiff ? undefined : styles.disabledDropdownIcon}> | ||
BrunoQuaresma marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{isDiffOpen ? <CloseDropdown /> : <OpenDropdown />} | ||
</div> | ||
</Stack> | ||
|
||
{shouldDisplayDiff && ( | ||
<Collapse in={isDiffOpen}> | ||
<AuditDiff diff={auditLog.diff} /> | ||
</Collapse> | ||
)} | ||
</TableCell> | ||
</TableRow> | ||
) | ||
} | ||
|
||
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, | ||
}, | ||
})) |
Uh oh!
There was an error while loading. Please reload this page.