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}`, + ) +}