Skip to content

Commit aa43f99

Browse files
feat(site): Promote template version (coder#6929)
1 parent fab8da6 commit aa43f99

File tree

11 files changed

+197
-31
lines changed

11 files changed

+197
-31
lines changed

site/src/AppRouter.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ const CreateTokenPage = lazy(
135135
const TemplateFilesPage = lazy(
136136
() => import("./pages/TemplatePage/TemplateFilesPage/TemplateFilesPage"),
137137
)
138+
const TemplateVersionsPage = lazy(
139+
() =>
140+
import("./pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage"),
141+
)
138142
const TemplateSchedulePage = lazy(
139143
() =>
140144
import(
@@ -170,8 +174,8 @@ export const AppRouter: FC = () => {
170174
<Route path=":template">
171175
<Route element={<TemplateLayout />}>
172176
<Route index element={<TemplateSummaryPage />} />
173-
174177
<Route path="files" element={<TemplateFilesPage />} />
178+
<Route path="versions" element={<TemplateVersionsPage />} />
175179
</Route>
176180

177181
<Route path="workspace" element={<CreateWorkspacePage />} />

site/src/components/TemplateLayout/TemplateLayout.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,17 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
123123
Source Code
124124
</NavLink>
125125
)}
126+
<NavLink
127+
to={`/templates/${templateName}/versions`}
128+
className={({ isActive }) =>
129+
combineClasses([
130+
styles.tabItem,
131+
isActive ? styles.tabItemActive : undefined,
132+
])
133+
}
134+
>
135+
Versions
136+
</NavLink>
126137
</Stack>
127138
</Margins>
128139
</div>

site/src/components/VersionsTable/VersionRow.tsx

+61-5
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,51 @@
1+
import Button from "@material-ui/core/Button"
12
import { makeStyles } from "@material-ui/core/styles"
23
import TableCell from "@material-ui/core/TableCell"
34
import { TemplateVersion } from "api/typesGenerated"
5+
import { Pill } from "components/Pill/Pill"
46
import { Stack } from "components/Stack/Stack"
57
import { TimelineEntry } from "components/Timeline/TimelineEntry"
68
import { UserAvatar } from "components/UserAvatar/UserAvatar"
7-
import { useClickable } from "hooks/useClickable"
9+
import { useClickableTableRow } from "hooks/useClickableTableRow"
810
import { useTranslation } from "react-i18next"
911
import { useNavigate } from "react-router-dom"
12+
import { colors } from "theme/colors"
13+
import { combineClasses } from "util/combineClasses"
1014

1115
export interface VersionRowProps {
1216
version: TemplateVersion
17+
isActive: boolean
18+
onPromoteClick?: (templateVersionId: string) => void
1319
}
1420

15-
export const VersionRow: React.FC<VersionRowProps> = ({ version }) => {
21+
export const VersionRow: React.FC<VersionRowProps> = ({
22+
version,
23+
isActive,
24+
onPromoteClick,
25+
}) => {
1626
const styles = useStyles()
1727
const { t } = useTranslation("templatePage")
1828
const navigate = useNavigate()
19-
const clickableProps = useClickable(() => {
20-
navigate(`versions/${version.name}`)
29+
const clickableProps = useClickableTableRow(() => {
30+
navigate(version.name)
2131
})
2232

2333
return (
24-
<TimelineEntry data-testid={`version-${version.id}`} {...clickableProps}>
34+
<TimelineEntry
35+
data-testid={`version-${version.id}`}
36+
{...clickableProps}
37+
className={combineClasses({
38+
[clickableProps.className]: true,
39+
[styles.row]: true,
40+
[styles.active]: isActive,
41+
})}
42+
>
2543
<TableCell className={styles.versionCell}>
2644
<Stack
2745
direction="row"
2846
alignItems="center"
2947
className={styles.versionWrapper}
48+
justifyContent="space-between"
3049
>
3150
<Stack direction="row" alignItems="center">
3251
<UserAvatar
@@ -49,17 +68,54 @@ export const VersionRow: React.FC<VersionRowProps> = ({ version }) => {
4968
</span>
5069
</Stack>
5170
</Stack>
71+
{isActive ? (
72+
<Pill text="Active version" type="success" />
73+
) : (
74+
onPromoteClick && (
75+
<Button
76+
size="small"
77+
variant="outlined"
78+
className={styles.promoteButton}
79+
onClick={(e) => {
80+
e.preventDefault()
81+
e.stopPropagation()
82+
onPromoteClick(version.id)
83+
}}
84+
>
85+
Promote version
86+
</Button>
87+
)
88+
)}
5289
</Stack>
5390
</TableCell>
5491
</TimelineEntry>
5592
)
5693
}
5794

5895
const useStyles = makeStyles((theme) => ({
96+
row: {
97+
"&:hover $promoteButton": {
98+
color: theme.palette.text.primary,
99+
borderColor: colors.gray[11],
100+
"&:hover": {
101+
borderColor: theme.palette.text.primary,
102+
},
103+
},
104+
},
105+
106+
promoteButton: {
107+
color: theme.palette.text.secondary,
108+
transition: "none",
109+
},
110+
59111
versionWrapper: {
60112
padding: theme.spacing(2, 4),
61113
},
62114

115+
active: {
116+
backgroundColor: theme.palette.background.paperLight,
117+
},
118+
63119
versionCell: {
64120
padding: "0 !important",
65121
position: "relative",

site/src/components/VersionsTable/VersionsTable.stories.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { action } from "@storybook/addon-actions"
12
import { ComponentMeta, Story } from "@storybook/react"
23
import { MockTemplateVersion } from "../../testHelpers/entities"
34
import { VersionsTable, VersionsTableProps } from "./VersionsTable"
@@ -13,13 +14,30 @@ const Template: Story<VersionsTableProps> = (args) => (
1314

1415
export const Example = Template.bind({})
1516
Example.args = {
17+
activeVersionId: MockTemplateVersion.id,
1618
versions: [
19+
{
20+
...MockTemplateVersion,
21+
id: "2",
22+
name: "test-template-version-2",
23+
created_at: "2022-05-18T18:39:01.382927298Z",
24+
},
1725
MockTemplateVersion,
26+
],
27+
}
28+
29+
export const CanPromote = Template.bind({})
30+
CanPromote.args = {
31+
activeVersionId: MockTemplateVersion.id,
32+
onPromoteClick: action("onPromoteClick"),
33+
versions: [
1834
{
1935
...MockTemplateVersion,
36+
id: "2",
2037
name: "test-template-version-2",
2138
created_at: "2022-05-18T18:39:01.382927298Z",
2239
},
40+
MockTemplateVersion,
2341
],
2442
}
2543

site/src/components/VersionsTable/VersionsTable.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,15 @@ export const Language = {
1919
}
2020

2121
export interface VersionsTableProps {
22+
activeVersionId: string
23+
onPromoteClick?: (templateVersionId: string) => void
2224
versions?: TypesGen.TemplateVersion[]
2325
}
2426

2527
export const VersionsTable: FC<React.PropsWithChildren<VersionsTableProps>> = ({
2628
versions,
29+
onPromoteClick,
30+
activeVersionId,
2731
}) => {
2832
return (
2933
<TableContainer>
@@ -34,7 +38,12 @@ export const VersionsTable: FC<React.PropsWithChildren<VersionsTableProps>> = ({
3438
items={versions.slice().reverse()}
3539
getDate={(version) => new Date(version.created_at)}
3640
row={(version) => (
37-
<VersionRow version={version} key={version.id} />
41+
<VersionRow
42+
onPromoteClick={onPromoteClick}
43+
version={version}
44+
key={version.id}
45+
isActive={activeVersionId === version.id}
46+
/>
3847
)}
3948
/>
4049
) : (

site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import { useOrganizationId } from "hooks/useOrganizationId"
88
import { useTab } from "hooks/useTab"
99
import { FC, useEffect } from "react"
1010
import { Helmet } from "react-helmet-async"
11-
import { pageTitle } from "util/page"
1211
import {
1312
getTemplateVersionFiles,
1413
TemplateVersionFiles,
1514
} from "util/templateVersion"
15+
import { getTemplatePageTitle } from "../utils"
1616

1717
const fetchTemplateFiles = async (
1818
organizationId: string,
@@ -80,7 +80,7 @@ const TemplateFilesPage: FC = () => {
8080
return (
8181
<>
8282
<Helmet>
83-
<title>{pageTitle(`${template?.name} · Source Code`)}</title>
83+
<title>{getTemplatePageTitle("Source Code", template)}</title>
8484
</Helmet>
8585

8686
{templateFiles && tab.isLoaded ? (

site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx

+2-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout"
22
import { FC } from "react"
33
import { Helmet } from "react-helmet-async"
4-
import { pageTitle } from "util/page"
4+
import { getTemplatePageTitle } from "../utils"
55
import { useTemplateSummaryData } from "./data"
66
import { TemplateSummaryPageView } from "./TemplateSummaryPageView"
77

@@ -15,15 +15,7 @@ export const TemplateSummaryPage: FC = () => {
1515
return (
1616
<>
1717
<Helmet>
18-
<title>
19-
{pageTitle(
20-
`${
21-
template.display_name.length > 0
22-
? template.display_name
23-
: template.name
24-
} · Template`,
25-
)}
26-
</title>
18+
<title>{getTemplatePageTitle("Template", template)}</title>
2719
</Helmet>
2820
<TemplateSummaryPageView
2921
data={data}

site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx

+1-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { MemoizedMarkdown } from "components/Markdown/Markdown"
99
import { Stack } from "components/Stack/Stack"
1010
import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable"
1111
import { TemplateStats } from "components/TemplateStats/TemplateStats"
12-
import { VersionsTable } from "components/VersionsTable/VersionsTable"
1312
import frontMatter from "front-matter"
1413
import { FC } from "react"
1514
import { DAUChart } from "../../../components/DAUChart/DAUChart"
@@ -32,7 +31,7 @@ export const TemplateSummaryPageView: FC<TemplateSummaryPageViewProps> = ({
3231
return <Loader />
3332
}
3433

35-
const { daus, resources, versions } = data
34+
const { daus, resources } = data
3635
const readme = frontMatter(activeVersion.readme)
3736

3837
const getStartedResources = (resources: WorkspaceResource[]) => {
@@ -53,8 +52,6 @@ export const TemplateSummaryPageView: FC<TemplateSummaryPageViewProps> = ({
5352
<MemoizedMarkdown>{readme.body}</MemoizedMarkdown>
5453
</div>
5554
</div>
56-
57-
<VersionsTable versions={versions} />
5855
</Stack>
5956
)
6057
}

site/src/pages/TemplatePage/TemplateSummaryPage/data.ts

+2-8
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
11
import { useQuery } from "@tanstack/react-query"
2-
import {
3-
getTemplateVersionResources,
4-
getTemplateVersions,
5-
getTemplateDAUs,
6-
} from "api/api"
2+
import { getTemplateVersionResources, getTemplateDAUs } from "api/api"
73

84
const fetchTemplateSummary = async (
95
templateId: string,
106
activeVersionId: string,
117
) => {
12-
const [resources, versions, daus] = await Promise.all([
8+
const [resources, daus] = await Promise.all([
139
getTemplateVersionResources(activeVersionId),
14-
getTemplateVersions(templateId),
1510
getTemplateDAUs(templateId),
1611
])
1712

1813
return {
1914
resources,
20-
versions,
2115
daus,
2216
}
2317
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { useMutation, useQuery } from "@tanstack/react-query"
2+
import { getTemplateVersions, updateActiveTemplateVersion } from "api/api"
3+
import { getErrorMessage } from "api/errors"
4+
import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
5+
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"
6+
import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout"
7+
import { VersionsTable } from "components/VersionsTable/VersionsTable"
8+
import { useState } from "react"
9+
import { Helmet } from "react-helmet-async"
10+
import { getTemplatePageTitle } from "../utils"
11+
import { useDashboard } from "components/Dashboard/DashboardProvider"
12+
13+
const TemplateVersionsPage = () => {
14+
const dashboard = useDashboard()
15+
const { template, permissions } = useTemplateLayoutContext()
16+
const { data } = useQuery({
17+
queryKey: ["template", "versions", template.id],
18+
queryFn: () => getTemplateVersions(template.id),
19+
})
20+
// We use this to update the active version in the UI without having to refetch the template
21+
const [latestActiveVersion, setLatestActiveVersion] = useState(
22+
template.active_version_id,
23+
)
24+
const { mutate: promoteVersion, isLoading: isPromoting } = useMutation({
25+
mutationFn: (templateVersionId: string) => {
26+
return updateActiveTemplateVersion(template.id, {
27+
id: templateVersionId,
28+
})
29+
},
30+
onSuccess: async () => {
31+
setLatestActiveVersion(selectedVersionIdToPromote as string)
32+
setSelectedVersionIdToPromote(undefined)
33+
displaySuccess("Version promoted successfully")
34+
},
35+
onError: (error) => {
36+
displayError(getErrorMessage(error, "Failed to promote version"))
37+
},
38+
})
39+
const [selectedVersionIdToPromote, setSelectedVersionIdToPromote] = useState<
40+
string | undefined
41+
>()
42+
const canPromoteVersion =
43+
dashboard.experiments.includes("template_editor") &&
44+
permissions.canUpdateTemplate
45+
46+
return (
47+
<>
48+
<Helmet>
49+
<title>{getTemplatePageTitle("Versions", template)}</title>
50+
</Helmet>
51+
<VersionsTable
52+
versions={data}
53+
onPromoteClick={
54+
canPromoteVersion ? setSelectedVersionIdToPromote : undefined
55+
}
56+
activeVersionId={latestActiveVersion}
57+
/>
58+
<ConfirmDialog
59+
type="info"
60+
hideCancel={false}
61+
open={selectedVersionIdToPromote !== undefined}
62+
onConfirm={() => {
63+
promoteVersion(selectedVersionIdToPromote as string)
64+
}}
65+
onClose={() => setSelectedVersionIdToPromote(undefined)}
66+
title="Promote version"
67+
confirmLoading={isPromoting}
68+
confirmText="Promote"
69+
description="Are you sure you want to promote this version? Workspaces will be prompted to “Update” to this version once promoted."
70+
/>
71+
</>
72+
)
73+
}
74+
75+
export default TemplateVersionsPage

0 commit comments

Comments
 (0)