refactor: Show template versions as timeline (#4800)

This commit is contained in:
Bruno Quaresma
2022-10-31 16:38:07 +00:00
committed by GitHub
parent cc655672eb
commit 46e0953876
21 changed files with 218 additions and 218 deletions
+1 -2
View File
@@ -656,8 +656,7 @@ export interface TemplateVersion {
readonly name: string
readonly job: ProvisionerJob
readonly readme: string
readonly created_by_id: string
readonly created_by_name: string
readonly created_by: User
}
// From codersdk/templates.go
@@ -1,46 +0,0 @@
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 BuildDateRow {
date: Date
}
export const BuildDateRow: FC<BuildDateRow> = ({ 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.buildDateRow}>
<TableCell
className={styles.buildDateCell}
title={date.toLocaleDateString()}
>
{displayDate}
</TableCell>
</TableRow>
)
}
const useStyles = makeStyles((theme) => ({
buildDateRow: {
background: theme.palette.background.paper,
"&:not(:first-child) td": {
borderTop: `1px solid ${theme.palette.divider}`,
},
},
buildDateCell: {
padding: `${theme.spacing(1, 4)} !important`,
background: `${theme.palette.background.paperLight} !important`,
fontSize: 12,
position: "relative",
color: theme.palette.text.secondary,
textTransform: "capitalize",
},
}))
+11 -47
View File
@@ -4,73 +4,37 @@ import TableBody from "@material-ui/core/TableBody"
import TableCell from "@material-ui/core/TableCell"
import TableContainer from "@material-ui/core/TableContainer"
import TableRow from "@material-ui/core/TableRow"
import { FC, Fragment } from "react"
import { Timeline } from "components/Timeline/Timeline"
import { FC } from "react"
import * as TypesGen from "../../api/typesGenerated"
import { EmptyState } from "../EmptyState/EmptyState"
import { TableLoader } from "../TableLoader/TableLoader"
import { BuildDateRow } from "./BuildDateRow"
import { BuildRow } from "./BuildRow"
export const Language = {
emptyMessage: "No builds found",
inProgressLabel: "In progress",
actionLabel: "Action",
durationLabel: "Duration",
startedAtLabel: "Started at",
statusLabel: "Status",
}
export interface BuildsTableProps {
builds?: TypesGen.WorkspaceBuild[]
}
const groupBuildsByDate = (builds?: TypesGen.WorkspaceBuild[]) => {
const buildsByDate: Record<string, TypesGen.WorkspaceBuild[]> = {}
if (!builds) {
return
}
builds.forEach((build) => {
const dateKey = new Date(build.created_at).toDateString()
// Unsure why this is here but we probably need to fix it.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- see above
if (buildsByDate[dateKey]) {
buildsByDate[dateKey].push(build)
} else {
buildsByDate[dateKey] = [build]
}
})
return buildsByDate
}
export const BuildsTable: FC<React.PropsWithChildren<BuildsTableProps>> = ({
builds,
}) => {
const isLoading = !builds
const buildsByDate = groupBuildsByDate(builds)
return (
<TableContainer>
<Table data-testid="builds-table" aria-describedby="builds table">
<TableBody>
{isLoading && <TableLoader />}
{buildsByDate &&
Object.keys(buildsByDate).map((dateStr) => {
const builds = buildsByDate[dateStr]
return (
<Fragment key={dateStr}>
<BuildDateRow date={new Date(dateStr)} />
{builds.map((build) => (
<BuildRow key={build.id} build={build} />
))}
</Fragment>
)
})}
{builds ? (
<Timeline
items={builds}
getDate={(build) => new Date(build.created_at)}
row={(build) => <BuildRow key={build.id} build={build} />}
/>
) : (
<TableLoader />
)}
{builds && builds.length === 0 && (
<TableRow>
+53
View File
@@ -0,0 +1,53 @@
import { TimelineDateRow } from "components/Timeline/TimelineDateRow"
import { Fragment } from "react"
type GetDateFn<TData> = (data: TData) => Date
const groupByDate = <TData,>(
items: TData[],
getDate: GetDateFn<TData>,
): Record<string, TData[]> => {
const itemsByDate: Record<string, TData[]> = {}
items.forEach((item) => {
const dateKey = getDate(item).toDateString()
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- Is not a guarantee a value is defined when access it dynamically
if (itemsByDate[dateKey]) {
itemsByDate[dateKey].push(item)
} else {
itemsByDate[dateKey] = [item]
}
})
return itemsByDate
}
export interface TimelineProps<TData> {
items: TData[]
getDate: GetDateFn<TData>
row: (item: TData) => JSX.Element
}
export const Timeline = <TData,>({
items,
getDate,
row,
}: TimelineProps<TData>): JSX.Element => {
const itemsByDate = groupByDate(items, getDate)
return (
<>
{Object.keys(itemsByDate).map((dateStr) => {
const items = itemsByDate[dateStr]
return (
<Fragment key={dateStr}>
<TimelineDateRow date={new Date(dateStr)} />
{items.map(row)}
</Fragment>
)
})}
</>
)
}
@@ -4,11 +4,11 @@ import TableRow from "@material-ui/core/TableRow"
import formatRelative from "date-fns/formatRelative"
import { FC } from "react"
export interface TableDateRow {
export interface TimelineDateRow {
date: Date
}
export const TableDateRow: FC<TableDateRow> = ({ date }) => {
export const TimelineDateRow: FC<TimelineDateRow> = ({ date }) => {
const styles = useStyles()
// We only want the message related to the date since the time is displayed
// inside of the build row
@@ -0,0 +1,88 @@
import { makeStyles } from "@material-ui/core/styles"
import TableCell from "@material-ui/core/TableCell"
import TableRow from "@material-ui/core/TableRow"
import { TemplateVersion } from "api/typesGenerated"
import { Stack } from "components/Stack/Stack"
import { UserAvatar } from "components/UserAvatar/UserAvatar"
import { useTranslation } from "react-i18next"
export interface VersionRowProps {
version: TemplateVersion
}
export const VersionRow: React.FC<VersionRowProps> = ({ version }) => {
const styles = useStyles()
const { t } = useTranslation("templatePage")
return (
<TableRow
className={styles.versionRow}
data-testid={`version-${version.id}`}
>
<TableCell className={styles.versionCell}>
<Stack
direction="row"
alignItems="center"
className={styles.versionWrapper}
>
<Stack direction="row" alignItems="center">
<UserAvatar
username={version.created_by.username}
avatarURL={version.created_by.avatar_url}
/>
<Stack
className={styles.versionSummary}
direction="row"
alignItems="center"
spacing={1}
>
<span>
<strong>{version.created_by.username}</strong>{" "}
{t("createdVersion")} <strong>{version.name}</strong>
</span>
<span className={styles.versionTime}>
{new Date(version.created_at).toLocaleTimeString()}
</span>
</Stack>
</Stack>
</Stack>
</TableCell>
</TableRow>
)
}
const useStyles = makeStyles((theme) => ({
versionRow: {
"&:not(:last-child) td:before": {
position: "absolute",
top: 20,
left: 50,
display: "block",
content: "''",
height: "100%",
width: 2,
background: theme.palette.divider,
},
},
versionWrapper: {
padding: theme.spacing(2, 4),
},
versionCell: {
padding: "0 !important",
position: "relative",
borderBottom: 0,
},
versionSummary: {
...theme.typography.body1,
fontFamily: "inherit",
},
versionTime: {
color: theme.palette.text.secondary,
fontSize: 12,
},
}))
@@ -1,16 +1,15 @@
import Box from "@material-ui/core/Box"
import { Theme } 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 useTheme from "@material-ui/styles/useTheme"
import { Timeline } from "components/Timeline/Timeline"
import { FC } from "react"
import * as TypesGen from "../../api/typesGenerated"
import { EmptyState } from "../EmptyState/EmptyState"
import { TableLoader } from "../TableLoader/TableLoader"
import { VersionRow } from "./VersionRow"
export const Language = {
emptyMessage: "No versions found",
@@ -26,45 +25,21 @@ export interface VersionsTableProps {
export const VersionsTable: FC<React.PropsWithChildren<VersionsTableProps>> = ({
versions,
}) => {
const isLoading = !versions
const theme: Theme = useTheme()
return (
<TableContainer>
<Table data-testid="versions-table">
<TableHead>
<TableRow>
<TableCell width="30%">{Language.nameLabel}</TableCell>
<TableCell width="30%">{Language.createdAtLabel}</TableCell>
<TableCell width="40%">{Language.createdByLabel}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading && <TableLoader />}
{versions &&
versions
.slice()
.reverse()
.map((version) => {
return (
<TableRow
key={version.id}
data-testid={`version-${version.id}`}
>
<TableCell>{version.name}</TableCell>
<TableCell>
<span style={{ color: theme.palette.text.secondary }}>
{new Date(version.created_at).toLocaleString()}
</span>
</TableCell>
<TableCell>
<span style={{ color: theme.palette.text.secondary }}>
{version.created_by_name}
</span>
</TableCell>
</TableRow>
)
})}
{versions ? (
<Timeline
items={versions.slice().reverse()}
getDate={(version) => new Date(version.created_at)}
row={(version) => (
<VersionRow version={version} key={version.id} />
)}
/>
) : (
<TableLoader />
)}
{versions && versions.length === 0 && (
<TableRow>
+2 -1
View File
@@ -1,3 +1,4 @@
{
"deleteSuccess": "Template successfully deleted."
"deleteSuccess": "Template successfully deleted.",
"createdVersion": "created the version"
}
+9 -37
View File
@@ -15,10 +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 { Timeline } from "components/Timeline/Timeline"
import { AuditHelpTooltip } from "components/Tooltips"
import { FC, Fragment } from "react"
import { FC } from "react"
import { PaginationMachineRef } from "xServices/pagination/paginationXService"
export const Language = {
@@ -37,27 +37,6 @@ 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 -- TODO look into this
if (auditLogsByDate[dateKey]) {
auditLogsByDate[dateKey].push(auditLog)
} else {
auditLogsByDate[dateKey] = [auditLog]
}
})
return auditLogsByDate
}
export interface AuditPageViewProps {
auditLogs?: AuditLog[]
count?: number
@@ -75,7 +54,6 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
}) => {
const isLoading = auditLogs === undefined || count === undefined
const isEmpty = !isLoading && auditLogs.length === 0
const auditLogsByDate = groupAuditLogsByDate(auditLogs)
return (
<Margins>
@@ -101,19 +79,13 @@ export const AuditPageView: FC<AuditPageViewProps> = ({
<TableBody>
{isLoading && <TableLoader />}
{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>
)
})}
{auditLogs && (
<Timeline
items={auditLogs}
getDate={(log) => new Date(log.time)}
row={(log) => <AuditLogRow key={log.id} auditLog={log} />}
/>
)}
{isEmpty && (
<TableRow>
+1 -2
View File
@@ -175,8 +175,7 @@ name:Template test
You can add instructions here
[Some link info](https://coder.com)`,
created_by_id: "test-creator-id",
created_by_name: "test_creator",
created_by: MockUser,
}
export const MockTemplate: TypesGen.Template = {