refactor: Make the audit log looks like a timeline (#4765)

This commit is contained in:
Bruno Quaresma
2022-10-26 19:20:26 +00:00
committed by GitHub
parent 9d8c3ca59c
commit 00495568e4
4 changed files with 195 additions and 78 deletions
+111 -64
View File
@@ -10,15 +10,19 @@ import {
import { Pill } from "components/Pill/Pill"
import { Stack } from "components/Stack/Stack"
import { UserAvatar } from "components/UserAvatar/UserAvatar"
import { ComponentProps, useState } from "react"
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
import { useState } from "react"
import { PaletteIndex } from "theme/palettes"
import userAgentParser from "ua-parser-js"
import { createDayString } from "util/createDayString"
import { combineClasses } from "util/combineClasses"
import { AuditLogDiff } from "./AuditLogDiff"
const pillTypeByHttpStatus = (
httpStatus: number,
): ComponentProps<typeof Pill>["type"] => {
const readableActionMessage = (auditLog: AuditLog) => {
return auditLog.description
.replace("{user}", `<strong>${auditLog.user?.username.trim()}</strong>`)
.replace("{target}", `<strong>${auditLog.resource_target.trim()}</strong>`)
}
const httpStatusColor = (httpStatus: number): PaletteIndex => {
if (httpStatus >= 300 && httpStatus < 500) {
return "warning"
}
@@ -30,12 +34,6 @@ const pillTypeByHttpStatus = (
return "success"
}
const readableActionMessage = (auditLog: AuditLog) => {
return auditLog.description
.replace("{user}", `<strong>${auditLog.user?.username}</strong>`)
.replace("{target}", `<strong>${auditLog.resource_target}</strong>`)
}
export interface AuditLogRowProps {
auditLog: AuditLog
// Useful for Storybook
@@ -63,13 +61,19 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
}
return (
<TableRow key={auditLog.id} data-testid={`audit-log-row-${auditLog.id}`}>
<TableRow
key={auditLog.id}
data-testid={`audit-log-row-${auditLog.id}`}
className={styles.auditLogRow}
>
<TableCell className={styles.auditLogCell}>
<Stack
style={{ cursor: shouldDisplayDiff ? "pointer" : undefined }}
direction="row"
alignItems="center"
className={styles.auditLogRow}
className={combineClasses({
[styles.auditLogHeader]: true,
[styles.clickable]: shouldDisplayDiff,
})}
tabIndex={0}
onClick={toggle}
onKeyDown={(event) => {
@@ -81,51 +85,61 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
className={styles.auditLogRowInfo}
className={styles.auditLogHeaderInfo}
>
<Stack direction="row" alignItems="center">
<Stack
direction="row"
alignItems="center"
className={styles.fullWidth}
>
<UserAvatar
username={auditLog.user?.username ?? ""}
avatarURL={auditLog.user?.avatar_url}
/>
<div>
<span
className={styles.auditLogResume}
dangerouslySetInnerHTML={{
__html: readableActionMessage(auditLog),
}}
/>
<span className={styles.auditLogTime}>
{createDayString(auditLog.time)}
</span>
</div>
</Stack>
<Stack
direction="column"
alignItems="flex-end"
spacing={1}
className={styles.auditLogRight}
>
<Pill
type={pillTypeByHttpStatus(auditLog.status_code)}
text={auditLog.status_code.toString()}
/>
<Stack
alignItems="baseline"
className={styles.fullWidth}
justifyContent="space-between"
direction="row"
alignItems="center"
className={styles.auditLogExtraInfo}
>
<div>
<strong>IP</strong> {auditLog.ip ?? notAvailableLabel}
</div>
<div>
<strong>OS</strong> {os.name ?? notAvailableLabel}
</div>
<div>
<strong>Browser</strong> {displayBrowserInfo}
</div>
<Stack
className={styles.auditLogSummary}
direction="row"
alignItems="baseline"
spacing={1}
>
<span
dangerouslySetInnerHTML={{
__html: readableActionMessage(auditLog),
}}
/>
<span className={styles.auditLogTime}>
{new Date(auditLog.time).toLocaleTimeString()}
</span>
</Stack>
<Stack direction="row" alignItems="center">
<Stack direction="row" spacing={1} alignItems="baseline">
<span className={styles.auditLogInfo}>
IP: <strong>{auditLog.ip ?? notAvailableLabel}</strong>
</span>
<span className={styles.auditLogInfo}>
OS: <strong>{os.name ?? notAvailableLabel}</strong>
</span>
<span className={styles.auditLogInfo}>
Browser: <strong>{displayBrowserInfo}</strong>
</span>
</Stack>
<Pill
className={styles.httpStatusPill}
type={httpStatusColor(auditLog.status_code)}
text={auditLog.status_code.toString()}
/>
</Stack>
</Stack>
</Stack>
</Stack>
@@ -150,27 +164,58 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
const useStyles = makeStyles((theme) => ({
auditLogCell: {
padding: "0 !important",
border: 0,
},
auditLogRow: {
position: "relative",
"&:focus": {
outlineStyle: "solid",
outlineOffset: -1,
outlineWidth: 2,
outlineColor: theme.palette.secondary.dark,
},
"&:not(:last-child) td:before": {
position: "absolute",
top: 20,
left: 50,
display: "block",
content: "''",
height: "100%",
width: 2,
background: theme.palette.divider,
},
},
auditLogHeader: {
padding: theme.spacing(2, 4),
},
clickable: {
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.action.hover,
},
},
auditLogRowInfo: {
auditLogHeaderInfo: {
flex: 1,
},
auditLogResume: {
auditLogSummary: {
...theme.typography.body1,
fontFamily: "inherit",
display: "block",
},
auditLogTime: {
color: theme.palette.text.secondary,
fontSize: 12,
},
auditLogInfo: {
...theme.typography.body2,
fontSize: 12,
fontFamily: "inherit",
@@ -178,18 +223,20 @@ const useStyles = makeStyles((theme) => ({
display: "block",
},
auditLogRight: {
width: "auto",
},
auditLogExtraInfo: {
...theme.typography.body2,
fontFamily: MONOSPACE_FONT_FAMILY,
color: theme.palette.text.secondary,
whiteSpace: "nowrap",
},
// offset the absence of the arrow icon on diff-less logs
columnWithoutDiff: {
marginLeft: "24px",
},
fullWidth: {
width: "100%",
},
httpStatusPill: {
fontSize: 10,
height: 20,
paddingLeft: 10,
paddingRight: 10,
fontWeight: 600,
},
}))
+2 -2
View File
@@ -43,7 +43,7 @@ export const BuildRow: React.FC<BuildRowProps> = ({ build }) => {
<BuildAvatar build={build} />
<div>
<Stack
className={styles.buildResume}
className={styles.buildSummary}
direction="row"
alignItems="center"
spacing={1}
@@ -113,7 +113,7 @@ const useStyles = makeStyles((theme) => ({
borderBottom: 0,
},
buildResume: {
buildSummary: {
...theme.typography.body1,
fontFamily: "inherit",
},
@@ -0,0 +1,43 @@
import { makeStyles } from "@material-ui/core/styles"
import TableCell from "@material-ui/core/TableCell"
import TableRow from "@material-ui/core/TableRow"
import formatRelative from "date-fns/formatRelative"
import { FC } from "react"
export interface TableDateRow {
date: Date
}
export const TableDateRow: FC<TableDateRow> = ({ date }) => {
const styles = useStyles()
// We only want the message related to the date since the time is displayed
// inside of the build row
const displayDate = formatRelative(date, new Date()).split("at")[0]
return (
<TableRow className={styles.dateRow}>
<TableCell className={styles.dateCell} title={date.toLocaleDateString()}>
{displayDate}
</TableCell>
</TableRow>
)
}
const useStyles = makeStyles((theme) => ({
dateRow: {
background: theme.palette.background.paper,
"&:not(:first-child) td": {
borderTop: `1px solid ${theme.palette.divider}`,
},
},
dateCell: {
padding: `${theme.spacing(1, 4)} !important`,
background: `${theme.palette.background.paperLight} !important`,
fontSize: 12,
position: "relative",
color: theme.palette.text.secondary,
textTransform: "capitalize",
},
}))
+39 -12
View File
@@ -2,7 +2,6 @@ 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/typesGenerated"
import { AuditLogRow } from "components/AuditLogRow/AuditLogRow"
@@ -16,9 +15,10 @@ import {
import { PaginationWidget } from "components/PaginationWidget/PaginationWidget"
import { SearchBarWithFilter } from "components/SearchBarWithFilter/SearchBarWithFilter"
import { Stack } from "components/Stack/Stack"
import { TableDateRow } from "components/TableDateRow/TableDateRow"
import { TableLoader } from "components/TableLoader/TableLoader"
import { AuditHelpTooltip } from "components/Tooltips"
import { FC } from "react"
import { FC, Fragment } from "react"
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
export const Language = {
@@ -37,6 +37,27 @@ const presetFilters = [
{ query: "resource_type:user action:delete", name: "Deleted users" },
]
const groupAuditLogsByDate = (auditLogs?: AuditLog[]) => {
const auditLogsByDate: Record<string, AuditLog[]> = {}
if (!auditLogs) {
return
}
auditLogs.forEach((auditLog) => {
const dateKey = new Date(auditLog.time).toDateString()
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (auditLogsByDate[dateKey]) {
auditLogsByDate[dateKey].push(auditLog)
} else {
auditLogsByDate[dateKey] = [auditLog]
}
})
return auditLogsByDate
}
export interface AuditPageViewProps {
auditLogs?: AuditLog[]
count?: number
@@ -54,7 +75,7 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
}) => {
const isLoading = auditLogs === undefined || count === undefined
const isEmpty = !isLoading && auditLogs.length === 0
const hasResults = !isLoading && auditLogs.length > 0
const auditLogsByDate = groupAuditLogsByDate(auditLogs)
return (
<Margins>
@@ -77,17 +98,23 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell style={{ paddingLeft: 32 }}>Logs</TableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading && <TableLoader />}
{hasResults &&
auditLogs.map((auditLog) => (
<AuditLogRow auditLog={auditLog} key={auditLog.id} />
))}
{auditLogsByDate &&
Object.keys(auditLogsByDate).map((dateStr) => {
const auditLogs = auditLogsByDate[dateStr]
return (
<Fragment key={dateStr}>
<TableDateRow date={new Date(dateStr)} />
{auditLogs.map((log) => (
<AuditLogRow key={log.id} auditLog={log} />
))}
</Fragment>
)
})}
{isEmpty && (
<TableRow>
<TableCell colSpan={999}>