diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx
index 1910440f0ad08..ce88dfc6c6e32 100644
--- a/site/src/AppRouter.tsx
+++ b/site/src/AppRouter.tsx
@@ -135,6 +135,10 @@ const CreateTokenPage = lazy(
const TemplateFilesPage = lazy(
() => import("./pages/TemplatePage/TemplateFilesPage/TemplateFilesPage"),
)
+const TemplateVersionsPage = lazy(
+ () =>
+ import("./pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage"),
+)
const TemplateSchedulePage = lazy(
() =>
import(
@@ -170,8 +174,8 @@ export const AppRouter: FC = () => {
}>
} />
-
} />
+ } />
} />
diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx
index c44d3ac5a9b6b..f9b91da40556d 100644
--- a/site/src/components/TemplateLayout/TemplateLayout.tsx
+++ b/site/src/components/TemplateLayout/TemplateLayout.tsx
@@ -123,6 +123,17 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
Source Code
)}
+
+ combineClasses([
+ styles.tabItem,
+ isActive ? styles.tabItemActive : undefined,
+ ])
+ }
+ >
+ Versions
+
diff --git a/site/src/components/VersionsTable/VersionRow.tsx b/site/src/components/VersionsTable/VersionRow.tsx
index e1cb777f7f979..fd5f1bb2ddd76 100644
--- a/site/src/components/VersionsTable/VersionRow.tsx
+++ b/site/src/components/VersionsTable/VersionRow.tsx
@@ -1,32 +1,51 @@
+import Button from "@material-ui/core/Button"
import { makeStyles } from "@material-ui/core/styles"
import TableCell from "@material-ui/core/TableCell"
import { TemplateVersion } from "api/typesGenerated"
+import { Pill } from "components/Pill/Pill"
import { Stack } from "components/Stack/Stack"
import { TimelineEntry } from "components/Timeline/TimelineEntry"
import { UserAvatar } from "components/UserAvatar/UserAvatar"
-import { useClickable } from "hooks/useClickable"
+import { useClickableTableRow } from "hooks/useClickableTableRow"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
+import { colors } from "theme/colors"
+import { combineClasses } from "util/combineClasses"
export interface VersionRowProps {
version: TemplateVersion
+ isActive: boolean
+ onPromoteClick?: (templateVersionId: string) => void
}
-export const VersionRow: React.FC = ({ version }) => {
+export const VersionRow: React.FC = ({
+ version,
+ isActive,
+ onPromoteClick,
+}) => {
const styles = useStyles()
const { t } = useTranslation("templatePage")
const navigate = useNavigate()
- const clickableProps = useClickable(() => {
- navigate(`versions/${version.name}`)
+ const clickableProps = useClickableTableRow(() => {
+ navigate(version.name)
})
return (
-
+
= ({ version }) => {
+ {isActive ? (
+
+ ) : (
+ onPromoteClick && (
+
+ )
+ )}
@@ -56,10 +93,29 @@ export const VersionRow: React.FC = ({ version }) => {
}
const useStyles = makeStyles((theme) => ({
+ row: {
+ "&:hover $promoteButton": {
+ color: theme.palette.text.primary,
+ borderColor: colors.gray[11],
+ "&:hover": {
+ borderColor: theme.palette.text.primary,
+ },
+ },
+ },
+
+ promoteButton: {
+ color: theme.palette.text.secondary,
+ transition: "none",
+ },
+
versionWrapper: {
padding: theme.spacing(2, 4),
},
+ active: {
+ backgroundColor: theme.palette.background.paperLight,
+ },
+
versionCell: {
padding: "0 !important",
position: "relative",
diff --git a/site/src/components/VersionsTable/VersionsTable.stories.tsx b/site/src/components/VersionsTable/VersionsTable.stories.tsx
index 9956d7d150784..7196bdbaa137e 100644
--- a/site/src/components/VersionsTable/VersionsTable.stories.tsx
+++ b/site/src/components/VersionsTable/VersionsTable.stories.tsx
@@ -1,3 +1,4 @@
+import { action } from "@storybook/addon-actions"
import { ComponentMeta, Story } from "@storybook/react"
import { MockTemplateVersion } from "../../testHelpers/entities"
import { VersionsTable, VersionsTableProps } from "./VersionsTable"
@@ -13,13 +14,30 @@ const Template: Story = (args) => (
export const Example = Template.bind({})
Example.args = {
+ activeVersionId: MockTemplateVersion.id,
versions: [
+ {
+ ...MockTemplateVersion,
+ id: "2",
+ name: "test-template-version-2",
+ created_at: "2022-05-18T18:39:01.382927298Z",
+ },
MockTemplateVersion,
+ ],
+}
+
+export const CanPromote = Template.bind({})
+CanPromote.args = {
+ activeVersionId: MockTemplateVersion.id,
+ onPromoteClick: action("onPromoteClick"),
+ versions: [
{
...MockTemplateVersion,
+ id: "2",
name: "test-template-version-2",
created_at: "2022-05-18T18:39:01.382927298Z",
},
+ MockTemplateVersion,
],
}
diff --git a/site/src/components/VersionsTable/VersionsTable.tsx b/site/src/components/VersionsTable/VersionsTable.tsx
index 9aabf87522de3..1854a109e977d 100644
--- a/site/src/components/VersionsTable/VersionsTable.tsx
+++ b/site/src/components/VersionsTable/VersionsTable.tsx
@@ -19,11 +19,15 @@ export const Language = {
}
export interface VersionsTableProps {
+ activeVersionId: string
+ onPromoteClick?: (templateVersionId: string) => void
versions?: TypesGen.TemplateVersion[]
}
export const VersionsTable: FC> = ({
versions,
+ onPromoteClick,
+ activeVersionId,
}) => {
return (
@@ -34,7 +38,12 @@ export const VersionsTable: FC> = ({
items={versions.slice().reverse()}
getDate={(version) => new Date(version.created_at)}
row={(version) => (
-
+
)}
/>
) : (
diff --git a/site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx b/site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx
index 88b0729aa5a03..bd00c166b7371 100644
--- a/site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx
+++ b/site/src/pages/TemplatePage/TemplateFilesPage/TemplateFilesPage.tsx
@@ -8,11 +8,11 @@ import { useOrganizationId } from "hooks/useOrganizationId"
import { useTab } from "hooks/useTab"
import { FC, useEffect } from "react"
import { Helmet } from "react-helmet-async"
-import { pageTitle } from "util/page"
import {
getTemplateVersionFiles,
TemplateVersionFiles,
} from "util/templateVersion"
+import { getTemplatePageTitle } from "../utils"
const fetchTemplateFiles = async (
organizationId: string,
@@ -80,7 +80,7 @@ const TemplateFilesPage: FC = () => {
return (
<>
- {pageTitle(`${template?.name} · Source Code`)}
+ {getTemplatePageTitle("Source Code", template)}
{templateFiles && tab.isLoaded ? (
diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx
index ef9359e618533..34faa71dd05fd 100644
--- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx
+++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx
@@ -1,7 +1,7 @@
import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout"
import { FC } from "react"
import { Helmet } from "react-helmet-async"
-import { pageTitle } from "util/page"
+import { getTemplatePageTitle } from "../utils"
import { useTemplateSummaryData } from "./data"
import { TemplateSummaryPageView } from "./TemplateSummaryPageView"
@@ -15,15 +15,7 @@ export const TemplateSummaryPage: FC = () => {
return (
<>
-
- {pageTitle(
- `${
- template.display_name.length > 0
- ? template.display_name
- : template.name
- } · Template`,
- )}
-
+ {getTemplatePageTitle("Template", template)}
= ({
return
}
- const { daus, resources, versions } = data
+ const { daus, resources } = data
const readme = frontMatter(activeVersion.readme)
const getStartedResources = (resources: WorkspaceResource[]) => {
@@ -53,8 +52,6 @@ export const TemplateSummaryPageView: FC = ({
{readme.body}
-
-
)
}
diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/data.ts b/site/src/pages/TemplatePage/TemplateSummaryPage/data.ts
index 6470f402693b9..f9457d7549c6c 100644
--- a/site/src/pages/TemplatePage/TemplateSummaryPage/data.ts
+++ b/site/src/pages/TemplatePage/TemplateSummaryPage/data.ts
@@ -1,23 +1,17 @@
import { useQuery } from "@tanstack/react-query"
-import {
- getTemplateVersionResources,
- getTemplateVersions,
- getTemplateDAUs,
-} from "api/api"
+import { getTemplateVersionResources, getTemplateDAUs } from "api/api"
const fetchTemplateSummary = async (
templateId: string,
activeVersionId: string,
) => {
- const [resources, versions, daus] = await Promise.all([
+ const [resources, daus] = await Promise.all([
getTemplateVersionResources(activeVersionId),
- getTemplateVersions(templateId),
getTemplateDAUs(templateId),
])
return {
resources,
- versions,
daus,
}
}
diff --git a/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx
new file mode 100644
index 0000000000000..d5b820e6e3cdd
--- /dev/null
+++ b/site/src/pages/TemplatePage/TemplateVersionsPage/TemplateVersionsPage.tsx
@@ -0,0 +1,75 @@
+import { useMutation, useQuery } from "@tanstack/react-query"
+import { getTemplateVersions, updateActiveTemplateVersion } from "api/api"
+import { getErrorMessage } from "api/errors"
+import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
+import { displayError, displaySuccess } from "components/GlobalSnackbar/utils"
+import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout"
+import { VersionsTable } from "components/VersionsTable/VersionsTable"
+import { useState } from "react"
+import { Helmet } from "react-helmet-async"
+import { getTemplatePageTitle } from "../utils"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
+
+const TemplateVersionsPage = () => {
+ const dashboard = useDashboard()
+ const { template, permissions } = useTemplateLayoutContext()
+ const { data } = useQuery({
+ queryKey: ["template", "versions", template.id],
+ queryFn: () => getTemplateVersions(template.id),
+ })
+ // We use this to update the active version in the UI without having to refetch the template
+ const [latestActiveVersion, setLatestActiveVersion] = useState(
+ template.active_version_id,
+ )
+ const { mutate: promoteVersion, isLoading: isPromoting } = useMutation({
+ mutationFn: (templateVersionId: string) => {
+ return updateActiveTemplateVersion(template.id, {
+ id: templateVersionId,
+ })
+ },
+ onSuccess: async () => {
+ setLatestActiveVersion(selectedVersionIdToPromote as string)
+ setSelectedVersionIdToPromote(undefined)
+ displaySuccess("Version promoted successfully")
+ },
+ onError: (error) => {
+ displayError(getErrorMessage(error, "Failed to promote version"))
+ },
+ })
+ const [selectedVersionIdToPromote, setSelectedVersionIdToPromote] = useState<
+ string | undefined
+ >()
+ const canPromoteVersion =
+ dashboard.experiments.includes("template_editor") &&
+ permissions.canUpdateTemplate
+
+ return (
+ <>
+
+ {getTemplatePageTitle("Versions", template)}
+
+
+ {
+ promoteVersion(selectedVersionIdToPromote as string)
+ }}
+ onClose={() => setSelectedVersionIdToPromote(undefined)}
+ title="Promote version"
+ confirmLoading={isPromoting}
+ confirmText="Promote"
+ description="Are you sure you want to promote this version? Workspaces will be prompted to “Update” to this version once promoted."
+ />
+ >
+ )
+}
+
+export default TemplateVersionsPage
diff --git a/site/src/pages/TemplatePage/utils.ts b/site/src/pages/TemplatePage/utils.ts
new file mode 100644
index 0000000000000..89e7a3321bbdd
--- /dev/null
+++ b/site/src/pages/TemplatePage/utils.ts
@@ -0,0 +1,10 @@
+import { Template } from "api/typesGenerated"
+import { pageTitle } from "util/page"
+
+export const getTemplatePageTitle = (title: string, template: Template) => {
+ return pageTitle(
+ `${
+ template.display_name.length > 0 ? template.display_name : template.name
+ } · ${title}`,
+ )
+}