Add audit links/kira pilot (#5156)

* got links working

* added translations

* fixed translation

* added translation for unavailable ip

* added support for group, template, user links

* cleaned up string

* added deleted label

* querying for workspace id

* remove prints

* fix/write tests

* PR feedback pt 1

* PR feedback part 2
This commit is contained in:
Kira Pilot
2022-12-02 15:14:45 -05:00
committed by GitHub
parent fa641554e8
commit 65407462d1
12 changed files with 298 additions and 99 deletions
+2
View File
@@ -67,6 +67,8 @@ export interface AuditLog {
readonly status_code: number
readonly additional_fields: Record<string, string>
readonly description: string
readonly resource_link: string
readonly is_deleted: boolean
readonly user?: User
}
@@ -0,0 +1,49 @@
import {
MockAuditLog,
MockAuditLogWithWorkspaceBuild,
} from "testHelpers/entities"
import { AuditLogDescription } from "./AuditLogDescription"
import { render } from "../../testHelpers/renderHelpers"
import { screen } from "@testing-library/react"
const getByTextContent = (text: string) => {
return screen.getByText((_, element) => {
const hasText = (element: Element | null) => element?.textContent === text
const elementHasText = hasText(element)
const childrenDontHaveText = Array.from(element?.children || []).every(
(child) => !hasText(child),
)
return elementHasText && childrenDontHaveText
})
}
describe("AuditLogDescription", () => {
it("renders the correct string for a workspace create audit log", async () => {
render(<AuditLogDescription auditLog={MockAuditLog} />)
expect(
getByTextContent("TestUser created workspace bruno-dev"),
).toBeDefined()
})
it("renders the correct string for a workspace_build stop audit log", async () => {
render(<AuditLogDescription auditLog={MockAuditLogWithWorkspaceBuild} />)
expect(
getByTextContent("TestUser stopped build for workspace test2"),
).toBeDefined()
})
it("renders the correct string for a workspace_build audit log with a duplicate word", async () => {
const AuditLogWithRepeat = {
...MockAuditLogWithWorkspaceBuild,
additional_fields: {
workspaceName: "workspace",
},
}
render(<AuditLogDescription auditLog={AuditLogWithRepeat} />)
expect(
getByTextContent("TestUser stopped build for workspace workspace"),
).toBeDefined()
})
})
@@ -0,0 +1,63 @@
import { FC } from "react"
import { AuditLog } from "api/typesGenerated"
import { Link as RouterLink } from "react-router-dom"
import Link from "@material-ui/core/Link"
import { makeStyles } from "@material-ui/core/styles"
import i18next from "i18next"
export const AuditLogDescription: FC<{ auditLog: AuditLog }> = ({
auditLog,
}): JSX.Element => {
const classes = useStyles()
const { t } = i18next
let target = auditLog.resource_target.trim()
// audit logs with a resource_type of workspace build use workspace name as a target
if (
auditLog.resource_type === "workspace_build" &&
auditLog.additional_fields.workspaceName
) {
target = auditLog.additional_fields.workspaceName.trim()
}
// SSH key entries have no links
if (auditLog.resource_type === "git_ssh_key") {
return (
<span>
{auditLog.description
.replace("{user}", `${auditLog.user?.username.trim()}`)
.replace("{target}", `${target}`)}
</span>
)
}
const truncatedDescription = auditLog.description
.replace("{user}", `${auditLog.user?.username.trim()}`)
.replace("{target}", "")
return (
<span>
{truncatedDescription}
{auditLog.resource_link ? (
<Link component={RouterLink} to={auditLog.resource_link}>
<strong>{target}</strong>
</Link>
) : (
<strong>{target}</strong>
)}
{auditLog.is_deleted && (
<span className={classes.deletedLabel}>
<> {t("auditLog:table.logRow.deletedLabel")}</>
</span>
)}
</span>
)
}
const useStyles = makeStyles((theme) => ({
deletedLabel: {
...theme.typography.caption,
color: theme.palette.text.secondary,
},
}))
@@ -9,6 +9,7 @@ import {
MockAuditLog,
MockAuditLog2,
MockAuditLogWithWorkspaceBuild,
MockAuditLogWithDeletedResource,
} from "testHelpers/entities"
import { AuditLogRow, AuditLogRowProps } from "./AuditLogRow"
@@ -66,3 +67,8 @@ export const WithWorkspaceBuild = Template.bind({})
WithWorkspaceBuild.args = {
auditLog: MockAuditLogWithWorkspaceBuild,
}
export const DeletedResource = Template.bind({})
DeletedResource.args = {
auditLog: MockAuditLogWithDeletedResource,
}
@@ -1,41 +0,0 @@
import { readableActionMessage } from "./AuditLogRow"
import {
MockAuditLog,
MockAuditLogWithWorkspaceBuild,
} from "testHelpers/entities"
describe("readableActionMessage()", () => {
it("renders the correct string for a workspaceBuild audit log", async () => {
// When
const friendlyString = readableActionMessage(MockAuditLogWithWorkspaceBuild)
// Then
expect(friendlyString).toBe(
"<strong>TestUser</strong> stopped workspace build for <strong>test2</strong>",
)
})
it("renders the correct string for a workspaceBuild audit log with a duplicate word", async () => {
// When
const AuditLogWithRepeat = {
...MockAuditLogWithWorkspaceBuild,
additional_fields: {
workspaceName: "workspace",
},
}
const friendlyString = readableActionMessage(AuditLogWithRepeat)
// Then
expect(friendlyString).toBe(
"<strong>TestUser</strong> stopped workspace build for <strong>workspace</strong>",
)
})
it("renders the correct string for a workspace audit log", async () => {
// When
const friendlyString = readableActionMessage(MockAuditLog)
// Then
expect(friendlyString).toBe(
"<strong>TestUser</strong> created workspace <strong>bruno-dev</strong>",
)
})
})
+20 -26
View File
@@ -14,22 +14,8 @@ import { useState } from "react"
import { PaletteIndex } from "theme/palettes"
import userAgentParser from "ua-parser-js"
import { AuditLogDiff } from "./AuditLogDiff"
export const readableActionMessage = (auditLog: AuditLog): string => {
let target = auditLog.resource_target.trim()
// audit logs with a resource_type of workspace build use workspace name as a target
if (
auditLog.resource_type === "workspace_build" &&
auditLog.additional_fields.workspaceName
) {
target = auditLog.additional_fields.workspaceName.trim()
}
return auditLog.description
.replace("{user}", `<strong>${auditLog.user?.username.trim()}</strong>`)
.replace("{target}", `<strong>${target}</strong>`)
}
import i18next from "i18next"
import { AuditLogDescription } from "./AuditLogDescription"
const httpStatusColor = (httpStatus: number): PaletteIndex => {
if (httpStatus >= 300 && httpStatus < 500) {
@@ -54,14 +40,14 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
defaultIsDiffOpen = false,
}) => {
const styles = useStyles()
const { t } = i18next
const [isDiffOpen, setIsDiffOpen] = useState(defaultIsDiffOpen)
const diffs = Object.entries(auditLog.diff)
const shouldDisplayDiff = diffs.length > 0
const { os, browser } = userAgentParser(auditLog.user_agent)
const notAvailableLabel = "Not available"
const displayBrowserInfo = browser.name
? `${browser.name} ${browser.version}`
: notAvailableLabel
: t("auditLog:table.logRow.notAvailable")
const toggle = () => {
if (shouldDisplayDiff) {
@@ -115,11 +101,7 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
alignItems="baseline"
spacing={1}
>
<span
dangerouslySetInnerHTML={{
__html: readableActionMessage(auditLog),
}}
/>
<AuditLogDescription auditLog={auditLog} />
<span className={styles.auditLogTime}>
{new Date(auditLog.time).toLocaleTimeString()}
</span>
@@ -128,15 +110,27 @@ export const AuditLogRow: React.FC<AuditLogRowProps> = ({
<Stack direction="row" alignItems="center">
<Stack direction="row" spacing={1} alignItems="baseline">
<span className={styles.auditLogInfo}>
IP: <strong>{auditLog.ip ?? notAvailableLabel}</strong>
<>{t("auditLog:table.logRow.ip")}</>
<strong>
{auditLog.ip
? auditLog.ip
: t("auditLog:table.logRow.notAvailable")}
</strong>
</span>
<span className={styles.auditLogInfo}>
OS: <strong>{os.name ?? notAvailableLabel}</strong>
<>{t("auditLog:table.logRow.os")}</>
<strong>
{os.name
? os.name
: // https://github.com/i18next/next-i18next/issues/1795
t<string>("auditLog:table.logRow.notAvailable")}
</strong>
</span>
<span className={styles.auditLogInfo}>
Browser: <strong>{displayBrowserInfo}</strong>
<>{t("auditLog:table.logRow.browser")}</>
<strong>{displayBrowserInfo}</strong>
</span>
</Stack>
+8 -1
View File
@@ -6,6 +6,13 @@
},
"table": {
"emptyPage": "No audit logs available on this page",
"noLogs": "No audit logs available"
"noLogs": "No audit logs available",
"logRow": {
"deletedLabel": "(deleted)",
"ip": "IP: ",
"os": "OS: ",
"browser": "Browser: ",
"notAvailable": "Not available"
}
}
}
+8 -1
View File
@@ -986,6 +986,8 @@ export const MockAuditLog: TypesGen.AuditLog = {
additional_fields: {},
description: "{user} created workspace {target}",
user: MockUser,
resource_link: "/@admin/bruno-dev",
is_deleted: false,
}
export const MockAuditLog2: TypesGen.AuditLog = {
@@ -1024,12 +1026,17 @@ export const MockAuditLogWithWorkspaceBuild: TypesGen.AuditLog = {
request_id: "61555889-2875-475c-8494-f7693dd5d75b",
action: "stop",
resource_type: "workspace_build",
description: "{user} stopped workspace build for {target}",
description: "{user} stopped build for workspace {target}",
additional_fields: {
workspaceName: "test2",
},
}
export const MockAuditLogWithDeletedResource: TypesGen.AuditLog = {
...MockAuditLog,
is_deleted: true,
}
export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = {
credits_consumed: 0,
budget: 100,