Skip to content

Commit 9db3385

Browse files
committed
Extract components and add storybook
1 parent f9a7ed7 commit 9db3385

File tree

4 files changed

+350
-290
lines changed

4 files changed

+350
-290
lines changed
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { makeStyles } from "@material-ui/core/styles"
2+
import { AuditLog } from "api/typesGenerated"
3+
import { colors } from "theme/colors"
4+
import { combineClasses } from "util/combineClasses"
5+
6+
const getDiffValue = (value: number | string | boolean) => {
7+
if (typeof value === "string") {
8+
return `"${value}"`
9+
}
10+
11+
return value.toString()
12+
}
13+
14+
export const AuditDiff: React.FC<{ diff: AuditLog["diff"] }> = ({ diff }) => {
15+
const styles = useStyles()
16+
const diffEntries = Object.entries(diff)
17+
18+
return (
19+
<div className={styles.diff}>
20+
<div className={combineClasses([styles.diffColumn, styles.diffOld])}>
21+
{diffEntries.map(([attrName, valueDiff], index) => (
22+
<div key={attrName} className={styles.diffRow}>
23+
<div className={styles.diffLine}>{index + 1}</div>
24+
<div className={styles.diffIcon}>-</div>
25+
<div className={styles.diffContent}>
26+
{attrName}:{" "}
27+
<span className={combineClasses([styles.diffValue, styles.diffValueOld])}>
28+
{getDiffValue(valueDiff.old)}
29+
</span>
30+
</div>
31+
</div>
32+
))}
33+
</div>
34+
<div className={combineClasses([styles.diffColumn, styles.diffNew])}>
35+
{diffEntries.map(([attrName, valueDiff], index) => (
36+
<div key={attrName} className={styles.diffRow}>
37+
<div className={styles.diffLine}>{index + 1}</div>
38+
<div className={styles.diffIcon}>+</div>
39+
<div className={styles.diffContent}>
40+
{attrName}:{" "}
41+
<span className={combineClasses([styles.diffValue, styles.diffValueNew])}>
42+
{getDiffValue(valueDiff.new)}
43+
</span>
44+
</div>
45+
</div>
46+
))}
47+
</div>
48+
</div>
49+
)
50+
}
51+
52+
const useStyles = makeStyles((theme) => ({
53+
diff: {
54+
display: "flex",
55+
alignItems: "flex-start",
56+
fontSize: theme.typography.body2.fontSize,
57+
borderTop: `1px solid ${theme.palette.divider}`,
58+
},
59+
60+
diffColumn: {
61+
flex: 1,
62+
paddingTop: theme.spacing(2),
63+
paddingBottom: theme.spacing(2.5),
64+
lineHeight: "160%",
65+
},
66+
67+
diffOld: {
68+
backgroundColor: theme.palette.error.dark,
69+
color: theme.palette.error.contrastText,
70+
},
71+
72+
diffRow: {
73+
display: "flex",
74+
alignItems: "baseline",
75+
},
76+
77+
diffLine: {
78+
opacity: 0.5,
79+
80+
width: theme.spacing(8),
81+
textAlign: "right",
82+
},
83+
84+
diffIcon: {
85+
width: theme.spacing(4),
86+
textAlign: "center",
87+
fontSize: theme.typography.body1.fontSize,
88+
},
89+
90+
diffContent: {},
91+
92+
diffNew: {
93+
backgroundColor: theme.palette.success.dark,
94+
color: theme.palette.success.contrastText,
95+
},
96+
97+
diffValue: {
98+
padding: 1,
99+
borderRadius: theme.shape.borderRadius / 2,
100+
},
101+
102+
diffValueOld: {
103+
backgroundColor: colors.red[12],
104+
},
105+
106+
diffValueNew: {
107+
backgroundColor: colors.green[12],
108+
},
109+
}))
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Table from "@material-ui/core/Table"
2+
import TableBody from "@material-ui/core/TableBody"
3+
import TableCell from "@material-ui/core/TableCell"
4+
import TableContainer from "@material-ui/core/TableContainer"
5+
import TableHead from "@material-ui/core/TableHead"
6+
import TableRow from "@material-ui/core/TableRow"
7+
import { ComponentMeta, Story } from "@storybook/react"
8+
import { MockAuditLog, MockAuditLog2 } from "testHelpers/entities"
9+
import { AuditLogRow, AuditLogRowProps } from "./AuditLogRow"
10+
11+
export default {
12+
title: "components/AuditLogRow",
13+
component: AuditLogRow,
14+
} as ComponentMeta<typeof AuditLogRow>
15+
16+
const Template: Story<AuditLogRowProps> = (args) => (
17+
<TableContainer>
18+
<Table>
19+
<TableHead>
20+
<TableRow>
21+
<TableCell>Logs</TableCell>
22+
</TableRow>
23+
</TableHead>
24+
<TableBody>
25+
<AuditLogRow {...args} />
26+
</TableBody>
27+
</Table>
28+
</TableContainer>
29+
)
30+
31+
export const NoDiff = Template.bind({})
32+
NoDiff.args = {
33+
auditLog: MockAuditLog,
34+
}
35+
36+
export const WithDiff = Template.bind({})
37+
WithDiff.args = {
38+
auditLog: MockAuditLog2,
39+
defaultIsDiffOpen: true,
40+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import Collapse from "@material-ui/core/Collapse"
2+
import Link from "@material-ui/core/Link"
3+
import { makeStyles } from "@material-ui/core/styles"
4+
import TableCell from "@material-ui/core/TableCell"
5+
import TableRow from "@material-ui/core/TableRow"
6+
import { AuditLog, Template, Workspace } from "api/typesGenerated"
7+
import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows"
8+
import { Pill } from "components/Pill/Pill"
9+
import { Stack } from "components/Stack/Stack"
10+
import { UserAvatar } from "components/UserAvatar/UserAvatar"
11+
import { useState } from "react"
12+
import { Link as RouterLink } from "react-router-dom"
13+
import { createDayString } from "util/createDayString"
14+
import { AuditDiff } from "./AuditLogDiff"
15+
16+
const getResourceLabel = (resource: AuditLog["resource"]): string => {
17+
if ("name" in resource) {
18+
return resource.name
19+
}
20+
21+
return resource.username
22+
}
23+
24+
const getResourceHref = (
25+
resource: AuditLog["resource"],
26+
resourceType: AuditLog["resource_type"],
27+
): string | undefined => {
28+
switch (resourceType) {
29+
case "user":
30+
return `/users`
31+
case "template":
32+
return `/templates/${(resource as Template).name}`
33+
case "workspace":
34+
return `/workspaces/@${(resource as Workspace).owner_name}/${(resource as Workspace).name}`
35+
case "organization":
36+
return
37+
}
38+
}
39+
40+
const ResourceLink: React.FC<{
41+
resource: AuditLog["resource"]
42+
resourceType: AuditLog["resource_type"]
43+
}> = ({ resource, resourceType }) => {
44+
const href = getResourceHref(resource, resourceType)
45+
const label = <strong>{getResourceLabel(resource)}</strong>
46+
47+
if (!href) {
48+
return label
49+
}
50+
51+
return (
52+
<Link component={RouterLink} to={href}>
53+
{label}
54+
</Link>
55+
)
56+
}
57+
58+
const actionLabelByAction: Record<AuditLog["action"], string> = {
59+
create: "created a new",
60+
write: "updated",
61+
delete: "deleted",
62+
}
63+
64+
const resourceLabelByResourceType: Record<AuditLog["resource_type"], string> = {
65+
organization: "organization",
66+
template: "template",
67+
template_version: "template version",
68+
user: "user",
69+
workspace: "workspace",
70+
}
71+
72+
const readableActionMessage = (auditLog: AuditLog) => {
73+
return `${actionLabelByAction[auditLog.action]} ${
74+
resourceLabelByResourceType[auditLog.resource_type]
75+
}`
76+
}
77+
78+
export interface AuditLogRowProps {
79+
auditLog: AuditLog
80+
// Useful for Storybook
81+
defaultIsDiffOpen?: boolean
82+
}
83+
84+
export const AuditLogRow: React.FC<AuditLogRowProps> = ({
85+
auditLog,
86+
defaultIsDiffOpen = false,
87+
}) => {
88+
const styles = useStyles()
89+
const [isDiffOpen, setIsDiffOpen] = useState(defaultIsDiffOpen)
90+
const diffs = Object.entries(auditLog.diff)
91+
const shouldDisplayDiff = diffs.length > 0
92+
93+
const toggle = () => {
94+
if (shouldDisplayDiff) {
95+
setIsDiffOpen((v) => !v)
96+
}
97+
}
98+
99+
return (
100+
<TableRow key={auditLog.id} hover={shouldDisplayDiff}>
101+
<TableCell className={styles.auditLogCell}>
102+
<Stack
103+
style={{ cursor: shouldDisplayDiff ? "pointer" : undefined }}
104+
direction="row"
105+
alignItems="center"
106+
className={styles.auditLogRow}
107+
tabIndex={0}
108+
onClick={toggle}
109+
onKeyDown={(event) => {
110+
if (event.key === "Enter") {
111+
toggle()
112+
}
113+
}}
114+
>
115+
<Stack
116+
direction="row"
117+
alignItems="center"
118+
justifyContent="space-between"
119+
className={styles.auditLogRowInfo}
120+
>
121+
<Stack direction="row" alignItems="center">
122+
<UserAvatar username={auditLog.user?.username ?? ""} />
123+
<div>
124+
<span className={styles.auditLogResume}>
125+
<strong>{auditLog.user?.username}</strong> {readableActionMessage(auditLog)}{" "}
126+
<ResourceLink
127+
resource={auditLog.resource}
128+
resourceType={auditLog.resource_type}
129+
/>
130+
</span>
131+
<span className={styles.auditLogTime}>{createDayString(auditLog.time)}</span>
132+
</div>
133+
</Stack>
134+
135+
<Stack direction="column" alignItems="flex-end" spacing={1}>
136+
<Pill type="success" text={auditLog.status_code.toString()} />
137+
<Stack direction="row" alignItems="center" className={styles.auditLogExtraInfo}>
138+
<div>
139+
<strong>IP</strong> {auditLog.ip}
140+
</div>
141+
<div>
142+
<strong>Agent</strong> {auditLog.user_agent}
143+
</div>
144+
</Stack>
145+
</Stack>
146+
</Stack>
147+
148+
<div className={shouldDisplayDiff ? undefined : styles.disabledDropdownIcon}>
149+
{isDiffOpen ? <CloseDropdown /> : <OpenDropdown />}
150+
</div>
151+
</Stack>
152+
153+
{shouldDisplayDiff && (
154+
<Collapse in={isDiffOpen}>
155+
<AuditDiff diff={auditLog.diff} />
156+
</Collapse>
157+
)}
158+
</TableCell>
159+
</TableRow>
160+
)
161+
}
162+
163+
const useStyles = makeStyles((theme) => ({
164+
auditLogCell: {
165+
padding: "0 !important",
166+
},
167+
168+
auditLogRow: {
169+
padding: theme.spacing(2, 4),
170+
},
171+
172+
auditLogRowInfo: {
173+
flex: 1,
174+
},
175+
176+
auditLogResume: {
177+
...theme.typography.body1,
178+
fontFamily: "inherit",
179+
display: "block",
180+
},
181+
182+
auditLogTime: {
183+
...theme.typography.body2,
184+
fontFamily: "inherit",
185+
color: theme.palette.text.secondary,
186+
display: "block",
187+
},
188+
189+
auditLogExtraInfo: {
190+
...theme.typography.body2,
191+
fontFamily: "inherit",
192+
color: theme.palette.text.secondary,
193+
},
194+
195+
disabledDropdownIcon: {
196+
opacity: 0.5,
197+
},
198+
}))

0 commit comments

Comments
 (0)