mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +08:00
feat(site): Promote template version (#6929)
This commit is contained in:
@@ -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 = () => {
|
||||
<Route path=":template">
|
||||
<Route element={<TemplateLayout />}>
|
||||
<Route index element={<TemplateSummaryPage />} />
|
||||
|
||||
<Route path="files" element={<TemplateFilesPage />} />
|
||||
<Route path="versions" element={<TemplateVersionsPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path="workspace" element={<CreateWorkspacePage />} />
|
||||
|
||||
@@ -123,6 +123,17 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
|
||||
Source Code
|
||||
</NavLink>
|
||||
)}
|
||||
<NavLink
|
||||
to={`/templates/${templateName}/versions`}
|
||||
className={({ isActive }) =>
|
||||
combineClasses([
|
||||
styles.tabItem,
|
||||
isActive ? styles.tabItemActive : undefined,
|
||||
])
|
||||
}
|
||||
>
|
||||
Versions
|
||||
</NavLink>
|
||||
</Stack>
|
||||
</Margins>
|
||||
</div>
|
||||
|
||||
@@ -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<VersionRowProps> = ({ version }) => {
|
||||
export const VersionRow: React.FC<VersionRowProps> = ({
|
||||
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 (
|
||||
<TimelineEntry data-testid={`version-${version.id}`} {...clickableProps}>
|
||||
<TimelineEntry
|
||||
data-testid={`version-${version.id}`}
|
||||
{...clickableProps}
|
||||
className={combineClasses({
|
||||
[clickableProps.className]: true,
|
||||
[styles.row]: true,
|
||||
[styles.active]: isActive,
|
||||
})}
|
||||
>
|
||||
<TableCell className={styles.versionCell}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
className={styles.versionWrapper}
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Stack direction="row" alignItems="center">
|
||||
<UserAvatar
|
||||
@@ -49,6 +68,24 @@ export const VersionRow: React.FC<VersionRowProps> = ({ version }) => {
|
||||
</span>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{isActive ? (
|
||||
<Pill text="Active version" type="success" />
|
||||
) : (
|
||||
onPromoteClick && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="outlined"
|
||||
className={styles.promoteButton}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onPromoteClick(version.id)
|
||||
}}
|
||||
>
|
||||
Promote version
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TimelineEntry>
|
||||
@@ -56,10 +93,29 @@ export const VersionRow: React.FC<VersionRowProps> = ({ 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",
|
||||
|
||||
@@ -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<VersionsTableProps> = (args) => (
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
activeVersionId: MockTemplateVersion.id,
|
||||
versions: [
|
||||
MockTemplateVersion,
|
||||
{
|
||||
...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,
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,15 @@ export const Language = {
|
||||
}
|
||||
|
||||
export interface VersionsTableProps {
|
||||
activeVersionId: string
|
||||
onPromoteClick?: (templateVersionId: string) => void
|
||||
versions?: TypesGen.TemplateVersion[]
|
||||
}
|
||||
|
||||
export const VersionsTable: FC<React.PropsWithChildren<VersionsTableProps>> = ({
|
||||
versions,
|
||||
onPromoteClick,
|
||||
activeVersionId,
|
||||
}) => {
|
||||
return (
|
||||
<TableContainer>
|
||||
@@ -34,7 +38,12 @@ export const VersionsTable: FC<React.PropsWithChildren<VersionsTableProps>> = ({
|
||||
items={versions.slice().reverse()}
|
||||
getDate={(version) => new Date(version.created_at)}
|
||||
row={(version) => (
|
||||
<VersionRow version={version} key={version.id} />
|
||||
<VersionRow
|
||||
onPromoteClick={onPromoteClick}
|
||||
version={version}
|
||||
key={version.id}
|
||||
isActive={activeVersionId === version.id}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle(`${template?.name} · Source Code`)}</title>
|
||||
<title>{getTemplatePageTitle("Source Code", template)}</title>
|
||||
</Helmet>
|
||||
|
||||
{templateFiles && tab.isLoaded ? (
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>
|
||||
{pageTitle(
|
||||
`${
|
||||
template.display_name.length > 0
|
||||
? template.display_name
|
||||
: template.name
|
||||
} · Template`,
|
||||
)}
|
||||
</title>
|
||||
<title>{getTemplatePageTitle("Template", template)}</title>
|
||||
</Helmet>
|
||||
<TemplateSummaryPageView
|
||||
data={data}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { MemoizedMarkdown } from "components/Markdown/Markdown"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable"
|
||||
import { TemplateStats } from "components/TemplateStats/TemplateStats"
|
||||
import { VersionsTable } from "components/VersionsTable/VersionsTable"
|
||||
import frontMatter from "front-matter"
|
||||
import { FC } from "react"
|
||||
import { DAUChart } from "../../../components/DAUChart/DAUChart"
|
||||
@@ -32,7 +31,7 @@ export const TemplateSummaryPageView: FC<TemplateSummaryPageViewProps> = ({
|
||||
return <Loader />
|
||||
}
|
||||
|
||||
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<TemplateSummaryPageViewProps> = ({
|
||||
<MemoizedMarkdown>{readme.body}</MemoizedMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VersionsTable versions={versions} />
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{getTemplatePageTitle("Versions", template)}</title>
|
||||
</Helmet>
|
||||
<VersionsTable
|
||||
versions={data}
|
||||
onPromoteClick={
|
||||
canPromoteVersion ? setSelectedVersionIdToPromote : undefined
|
||||
}
|
||||
activeVersionId={latestActiveVersion}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
type="info"
|
||||
hideCancel={false}
|
||||
open={selectedVersionIdToPromote !== undefined}
|
||||
onConfirm={() => {
|
||||
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
|
||||
@@ -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}`,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user