feat(site): Promote template version (#6929)

This commit is contained in:
Bruno Quaresma
2023-04-03 14:27:17 +00:00
committed by GitHub
parent fab8da633b
commit aa43f998d4
11 changed files with 198 additions and 32 deletions
+5 -1
View File
@@ -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
+10
View File
@@ -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}`,
)
}