diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx index 69a14a113b0fb..6bce538c20373 100644 --- a/site/src/AppRouter.tsx +++ b/site/src/AppRouter.tsx @@ -125,10 +125,12 @@ const TemplateVariablesPage = lazy( const WorkspaceSettingsPage = lazy( () => import("./pages/WorkspaceSettingsPage/WorkspaceSettingsPage"), ) - const CreateTokenPage = lazy( () => import("./pages/CreateTokenPage/CreateTokenPage"), ) +const TemplateFilesPage = lazy( + () => import("./pages/TemplateFilesPage/TemplateFilesPage"), +) export const AppRouter: FC = () => { return ( @@ -162,6 +164,7 @@ export const AppRouter: FC = () => { path="permissions" element={} /> + } /> } /> diff --git a/site/src/components/TemplateFiles/TemplateFiles.tsx b/site/src/components/TemplateFiles/TemplateFiles.tsx new file mode 100644 index 0000000000000..d235a44eeb1b0 --- /dev/null +++ b/site/src/components/TemplateFiles/TemplateFiles.tsx @@ -0,0 +1,150 @@ +import { makeStyles } from "@material-ui/core/styles" +import { DockerIcon } from "components/Icons/DockerIcon" +import { MarkdownIcon } from "components/Icons/MarkdownIcon" +import { TerraformIcon } from "components/Icons/TerraformIcon" +import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter" +import { UseTabResult } from "hooks/useTab" +import { FC } from "react" +import { combineClasses } from "util/combineClasses" +import { TemplateVersionFiles } from "util/templateVersion" + +const iconByExtension: Record = { + tf: , + md: , + mkd: , + Dockerfile: , +} + +const getExtension = (filename: string) => { + if (filename.includes(".")) { + const [_, extension] = filename.split(".") + return extension + } + + return filename +} + +const languageByExtension: Record = { + tf: "hcl", + md: "markdown", + mkd: "markdown", + Dockerfile: "dockerfile", +} + +export const TemplateFiles: FC<{ + currentFiles: TemplateVersionFiles + previousFiles?: TemplateVersionFiles + tab: UseTabResult +}> = ({ currentFiles, previousFiles, tab }) => { + const styles = useStyles() + const filenames = Object.keys(currentFiles) + const selectedFilename = filenames[Number(tab.value)] + const currentFile = currentFiles[selectedFilename] + const previousFile = previousFiles && previousFiles[selectedFilename] + + return ( +
+
+ {filenames.map((filename, index) => { + const tabValue = index.toString() + const extension = getExtension(filename) + const icon = iconByExtension[extension] + const hasDiff = + previousFiles && + previousFiles[filename] && + currentFiles[filename] !== previousFiles[filename] + + return ( + + ) + })} +
+ + +
+ ) +} +const useStyles = makeStyles((theme) => ({ + tabs: { + display: "flex", + alignItems: "baseline", + borderBottom: `1px solid ${theme.palette.divider}`, + gap: 1, + }, + + tab: { + background: "transparent", + border: 0, + padding: theme.spacing(0, 3), + display: "flex", + alignItems: "center", + height: theme.spacing(6), + opacity: 0.85, + cursor: "pointer", + gap: theme.spacing(0.5), + position: "relative", + + "& svg": { + width: 22, + maxHeight: 16, + }, + + "&:hover": { + backgroundColor: theme.palette.action.hover, + }, + }, + + tabActive: { + opacity: 1, + background: theme.palette.action.hover, + + "&:after": { + content: '""', + display: "block", + height: 1, + width: "100%", + bottom: 0, + left: 0, + backgroundColor: theme.palette.secondary.dark, + position: "absolute", + }, + }, + + tabDiff: { + height: 6, + width: 6, + backgroundColor: theme.palette.warning.light, + borderRadius: "100%", + marginLeft: theme.spacing(0.5), + }, + + codeWrapper: { + background: theme.palette.background.paperLight, + }, + + files: { + borderRadius: theme.shape.borderRadius, + border: `1px solid ${theme.palette.divider}`, + }, + + prism: { + borderRadius: 0, + }, +})) diff --git a/site/src/components/TemplateLayout/TemplateLayout.tsx b/site/src/components/TemplateLayout/TemplateLayout.tsx index 4f05cf3d077f4..547c927b9395a 100644 --- a/site/src/components/TemplateLayout/TemplateLayout.tsx +++ b/site/src/components/TemplateLayout/TemplateLayout.tsx @@ -1,36 +1,56 @@ import { makeStyles } from "@material-ui/core/styles" -import { useMachine } from "@xstate/react" import { useOrganizationId } from "hooks/useOrganizationId" import { createContext, FC, Suspense, useContext } from "react" import { NavLink, Outlet, useNavigate, useParams } from "react-router-dom" import { combineClasses } from "util/combineClasses" -import { - TemplateContext, - templateMachine, -} from "xServices/template/templateXService" import { Margins } from "components/Margins/Margins" import { Stack } from "components/Stack/Stack" -import { Permissions } from "xServices/auth/authXService" import { Loader } from "components/Loader/Loader" -import { usePermissions } from "hooks/usePermissions" import { TemplatePageHeader } from "./TemplatePageHeader" import { AlertBanner } from "components/AlertBanner/AlertBanner" +import { + checkAuthorization, + getTemplateByName, + getTemplateVersion, +} from "api/api" +import { useQuery } from "@tanstack/react-query" +import { useDashboard } from "components/Dashboard/DashboardProvider" + +const templatePermissions = (templateId: string) => ({ + canUpdateTemplate: { + object: { + resource_type: "template", + resource_id: templateId, + }, + action: "update", + }, +}) -const useTemplateName = () => { - const { template } = useParams() +const fetchTemplate = async (orgId: string, templateName: string) => { + const template = await getTemplateByName(orgId, templateName) + const [activeVersion, permissions] = await Promise.all([ + getTemplateVersion(template.active_version_id), + checkAuthorization({ + checks: templatePermissions(template.id), + }), + ]) - if (!template) { - throw new Error("No template found in the URL") + return { + template, + activeVersion, + permissions, } - - return template } -type TemplateLayoutContextValue = { - context: TemplateContext - permissions?: Permissions +const useTemplateData = (orgId: string, templateName: string) => { + return useQuery({ + queryKey: ["template", templateName], + queryFn: () => fetchTemplate(orgId, templateName), + }) } +type TemplateLayoutContextValue = Awaited> + const TemplateLayoutContext = createContext< TemplateLayoutContextValue | undefined >(undefined) @@ -50,38 +70,30 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ }) => { const navigate = useNavigate() const styles = useStyles() - const organizationId = useOrganizationId() - const templateName = useTemplateName() - const [templateState, _] = useMachine(templateMachine, { - context: { - templateName, - organizationId, - }, - }) - const { - template, - permissions: templatePermissions, - getTemplateError, - } = templateState.context - const permissions = usePermissions() + const orgId = useOrganizationId() + const { template } = useParams() as { template: string } + const templateData = useTemplateData(orgId, template) + const dashboard = useDashboard() - if (getTemplateError) { + if (templateData.error) { return (
- +
) } - if (!template || !templatePermissions) { + if (templateData.isLoading || !templateData.data) { return } return ( <> { navigate("/templates") }} @@ -92,7 +104,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ combineClasses([ styles.tabItem, @@ -103,7 +115,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ Summary combineClasses([ styles.tabItem, @@ -113,14 +125,23 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({ > Permissions + + combineClasses([ + styles.tabItem, + isActive ? styles.tabItemActive : undefined, + ]) + } + > + Source Code + - + }>{children} diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx index 07d5334961ea6..1e01abbf5b830 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.stories.tsx @@ -1,5 +1,5 @@ import { ComponentMeta, Story } from "@storybook/react" -import { MockTemplate } from "testHelpers/entities" +import { MockTemplate, MockTemplateVersion } from "testHelpers/entities" import { TemplatePageHeader, TemplatePageHeaderProps, @@ -12,6 +12,9 @@ export default { template: { defaultValue: MockTemplate, }, + activeVersion: { + defaultValue: MockTemplateVersion, + }, permissions: { defaultValue: { canUpdateTemplate: true, diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.tsx index fdb43aa4875f3..503c09e944d51 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.tsx @@ -1,9 +1,10 @@ import Button from "@material-ui/core/Button" -import DeleteOutlined from "@material-ui/icons/DeleteOutlined" import AddCircleOutline from "@material-ui/icons/AddCircleOutline" -import SettingsOutlined from "@material-ui/icons/SettingsOutlined" -import CodeOutlined from "@material-ui/icons/CodeOutlined" -import { AuthorizationResponse, Template } from "api/typesGenerated" +import { + AuthorizationResponse, + Template, + TemplateVersion, +} from "api/typesGenerated" import { Avatar } from "components/Avatar/Avatar" import { Maybe } from "components/Conditionals/Maybe" import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog" @@ -13,44 +14,87 @@ import { PageHeaderSubtitle, } from "components/PageHeader/PageHeader" import { Stack } from "components/Stack/Stack" -import { FC } from "react" +import { FC, useState } from "react" import { Link as RouterLink } from "react-router-dom" import { useDeleteTemplate } from "./deleteTemplate" import { Margins } from "components/Margins/Margins" +import MoreVertOutlined from "@material-ui/icons/MoreVertOutlined" +import Menu from "@material-ui/core/Menu" +import MenuItem from "@material-ui/core/MenuItem" const Language = { - editButton: "Edit", variablesButton: "Variables", settingsButton: "Settings", createButton: "Create workspace", deleteButton: "Delete", + editFilesButton: "Edit files", } -const TemplateSettingsButton: FC<{ templateName: string }> = ({ - templateName, -}) => ( - -) +const TemplateMenu: FC<{ + templateName: string + templateVersion: string + canEditFiles: boolean + onDelete: () => void +}> = ({ templateName, templateVersion, canEditFiles, onDelete }) => { + const [anchorEl, setAnchorEl] = useState(null) -const TemplateVariablesButton: FC<{ templateName: string }> = ({ - templateName, -}) => ( - -) + const handleClose = () => { + setAnchorEl(null) + } + + return ( +
+ + + + + {Language.settingsButton} + + + {Language.variablesButton} + + {canEditFiles && ( + + {Language.editFilesButton} + + )} + { + onDelete() + handleClose() + }} + > + {Language.deleteButton} + + +
+ ) +} const CreateWorkspaceButton: FC<{ templateName: string @@ -65,21 +109,19 @@ const CreateWorkspaceButton: FC<{ ) -const DeleteTemplateButton: FC<{ onClick: () => void }> = ({ onClick }) => ( - -) - export type TemplatePageHeaderProps = { template: Template + activeVersion: TemplateVersion permissions: AuthorizationResponse + canEditFiles: boolean onDeleteTemplate: () => void } export const TemplatePageHeader: FC = ({ template, + activeVersion, permissions, + canEditFiles, onDeleteTemplate, }) => { const hasIcon = template.icon && template.icon !== "" @@ -90,14 +132,15 @@ export const TemplatePageHeader: FC = ({ + - - - - } > diff --git a/site/src/pages/TemplateFilesPage/TemplateFilesPage.tsx b/site/src/pages/TemplateFilesPage/TemplateFilesPage.tsx new file mode 100644 index 0000000000000..f5af6182a5734 --- /dev/null +++ b/site/src/pages/TemplateFilesPage/TemplateFilesPage.tsx @@ -0,0 +1,76 @@ +import { useQuery } from "@tanstack/react-query" +import { getPreviousTemplateVersionByName } from "api/api" +import { TemplateVersion } from "api/typesGenerated" +import { Loader } from "components/Loader/Loader" +import { TemplateFiles } from "components/TemplateFiles/TemplateFiles" +import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayout" +import { useOrganizationId } from "hooks/useOrganizationId" +import { useTab } from "hooks/useTab" +import { FC } from "react" +import { Helmet } from "react-helmet-async" +import { pageTitle } from "util/page" +import { getTemplateVersionFiles } from "util/templateVersion" + +const fetchTemplateFiles = async ( + organizationId: string, + templateName: string, + activeVersion: TemplateVersion, +) => { + const previousVersion = await getPreviousTemplateVersionByName( + organizationId, + templateName, + activeVersion.name, + ) + const loadFilesPromises: ReturnType[] = [] + loadFilesPromises.push(getTemplateVersionFiles(activeVersion)) + if (previousVersion) { + loadFilesPromises.push(getTemplateVersionFiles(previousVersion)) + } + const [currentFiles, previousFiles] = await Promise.all(loadFilesPromises) + return { + currentFiles, + previousFiles, + } +} + +const useTemplateFiles = ( + organizationId: string, + templateName: string, + activeVersion: TemplateVersion, +) => + useQuery({ + queryKey: ["templateFiles", templateName], + queryFn: () => + fetchTemplateFiles(organizationId, templateName, activeVersion), + }) + +const TemplateFilesPage: FC = () => { + const { template, activeVersion } = useTemplateLayoutContext() + const orgId = useOrganizationId() + const tab = useTab("file", "0") + const { data: templateFiles } = useTemplateFiles( + orgId, + template.name, + activeVersion, + ) + + return ( + <> + + {pageTitle(`${template?.name} ยท Source Code`)} + + + {templateFiles ? ( + + ) : ( + + )} + + ) +} + +export default TemplateFilesPage diff --git a/site/src/pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPage.tsx b/site/src/pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPage.tsx index a71ab12d024cf..56b4e36793e75 100644 --- a/site/src/pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPage.tsx +++ b/site/src/pages/TemplatePage/TemplatePermissionsPage/TemplatePermissionsPage.tsx @@ -18,11 +18,10 @@ export const TemplatePermissionsPage: FC< React.PropsWithChildren > = () => { const organizationId = useOrganizationId() - const { context } = useTemplateLayoutContext() - const { template, permissions } = context + const { template, permissions } = useTemplateLayoutContext() const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility() const [state, send] = useMachine(templateACLMachine, { - context: { templateId: template?.id }, + context: { templateId: template.id }, }) const { templateACL, userToBeUpdated, groupToBeUpdated } = state.context diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx index 42cbd7dd63ae8..ef9359e618533 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage.tsx @@ -2,22 +2,15 @@ import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayo import { FC } from "react" import { Helmet } from "react-helmet-async" import { pageTitle } from "util/page" +import { useTemplateSummaryData } from "./data" import { TemplateSummaryPageView } from "./TemplateSummaryPageView" -import { Loader } from "components/Loader/Loader" export const TemplateSummaryPage: FC = () => { - const { context } = useTemplateLayoutContext() - const { - template, - activeTemplateVersion, - templateResources, - templateVersions, - templateDAUs, - } = context - - if (!template || !activeTemplateVersion || !templateResources) { - return - } + const { template, activeVersion } = useTemplateLayoutContext() + const { data } = useTemplateSummaryData( + template.id, + template.active_version_id, + ) return ( <> @@ -33,11 +26,9 @@ export const TemplateSummaryPage: FC = () => { ) diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx index f3f348695ddea..c4c82debb3048 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.stories.tsx @@ -17,47 +17,47 @@ const Template: Story = (args) => ( export const Example = Template.bind({}) Example.args = { template: Mocks.MockTemplate, - activeTemplateVersion: Mocks.MockTemplateVersion, - templateResources: [ - Mocks.MockWorkspaceResource, - Mocks.MockWorkspaceResource2, - ], - templateVersions: [Mocks.MockTemplateVersion], + activeVersion: Mocks.MockTemplateVersion, + data: { + resources: [Mocks.MockWorkspaceResource, Mocks.MockWorkspaceResource2], + versions: [Mocks.MockTemplateVersion], + daus: Mocks.MockTemplateDAUResponse, + }, } export const NoIcon = Template.bind({}) NoIcon.args = { template: { ...Mocks.MockTemplate, icon: "" }, - activeTemplateVersion: Mocks.MockTemplateVersion, - templateResources: [ - Mocks.MockWorkspaceResource, - Mocks.MockWorkspaceResource2, - ], - templateVersions: [Mocks.MockTemplateVersion], + activeVersion: Mocks.MockTemplateVersion, + data: { + resources: [Mocks.MockWorkspaceResource, Mocks.MockWorkspaceResource2], + versions: [Mocks.MockTemplateVersion], + daus: Mocks.MockTemplateDAUResponse, + }, } export const SmallViewport = Template.bind({}) SmallViewport.args = { template: Mocks.MockTemplate, - activeTemplateVersion: { + activeVersion: { ...Mocks.MockTemplateVersion, readme: `--- -name:Template test ---- -## Instructions -You can add instructions here + name:Template test + --- + ## Instructions + You can add instructions here -[Some link info](https://coder.com) -\`\`\` -# This is a really long sentence to test that the code block wraps into a new line properly. -\`\`\` -`, + [Some link info](https://coder.com) + \`\`\` + # This is a really long sentence to test that the code block wraps into a new line properly. + \`\`\` + `, + }, + data: { + resources: [Mocks.MockWorkspaceResource, Mocks.MockWorkspaceResource2], + versions: [Mocks.MockTemplateVersion], + daus: Mocks.MockTemplateDAUResponse, }, - templateResources: [ - Mocks.MockWorkspaceResource, - Mocks.MockWorkspaceResource2, - ], - templateVersions: [Mocks.MockTemplateVersion], } SmallViewport.parameters = { chromatic: { viewports: [600] }, diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx index e04e9e18eb83c..63178a08ad74c 100644 --- a/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPageView.tsx @@ -1,10 +1,10 @@ import { makeStyles } from "@material-ui/core/styles" import { Template, - TemplateDAUsResponse, TemplateVersion, WorkspaceResource, } from "api/typesGenerated" +import { Loader } from "components/Loader/Loader" import { MemoizedMarkdown } from "components/Markdown/Markdown" import { Stack } from "components/Stack/Stack" import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable" @@ -13,26 +13,27 @@ import { VersionsTable } from "components/VersionsTable/VersionsTable" import frontMatter from "front-matter" import { FC } from "react" import { DAUChart } from "../../../components/DAUChart/DAUChart" +import { TemplateSummaryData } from "./data" export interface TemplateSummaryPageViewProps { + data?: TemplateSummaryData template: Template - activeTemplateVersion: TemplateVersion - templateResources: WorkspaceResource[] - templateVersions?: TemplateVersion[] - templateDAUs?: TemplateDAUsResponse + activeVersion: TemplateVersion } -export const TemplateSummaryPageView: FC< - React.PropsWithChildren -> = ({ +export const TemplateSummaryPageView: FC = ({ + data, template, - activeTemplateVersion, - templateResources, - templateVersions, - templateDAUs, + activeVersion, }) => { const styles = useStyles() - const readme = frontMatter(activeTemplateVersion.readme) + + if (!data) { + return + } + + const { daus, resources, versions } = data + const readme = frontMatter(activeVersion.readme) const getStartedResources = (resources: WorkspaceResource[]) => { return resources.filter( @@ -42,14 +43,9 @@ export const TemplateSummaryPageView: FC< return ( - - {templateDAUs && } - + + {daus && } +
README.md
@@ -58,7 +54,7 @@ export const TemplateSummaryPageView: FC<
- +
) } diff --git a/site/src/pages/TemplatePage/TemplateSummaryPage/data.ts b/site/src/pages/TemplatePage/TemplateSummaryPage/data.ts new file mode 100644 index 0000000000000..6470f402693b9 --- /dev/null +++ b/site/src/pages/TemplatePage/TemplateSummaryPage/data.ts @@ -0,0 +1,37 @@ +import { useQuery } from "@tanstack/react-query" +import { + getTemplateVersionResources, + getTemplateVersions, + getTemplateDAUs, +} from "api/api" + +const fetchTemplateSummary = async ( + templateId: string, + activeVersionId: string, +) => { + const [resources, versions, daus] = await Promise.all([ + getTemplateVersionResources(activeVersionId), + getTemplateVersions(templateId), + getTemplateDAUs(templateId), + ]) + + return { + resources, + versions, + daus, + } +} + +export const useTemplateSummaryData = ( + templateId: string, + activeVersionId: string, +) => { + return useQuery({ + queryKey: ["template", templateId, "summary"], + queryFn: () => fetchTemplateSummary(templateId, activeVersionId), + }) +} + +export type TemplateSummaryData = Awaited< + ReturnType +> diff --git a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx index dd0291fb4b954..abdf45edf415a 100644 --- a/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx +++ b/site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx @@ -1,11 +1,7 @@ import Button from "@material-ui/core/Button" import Link from "@material-ui/core/Link" -import { makeStyles } from "@material-ui/core/styles" import EditIcon from "@material-ui/icons/Edit" import { AlertBanner } from "components/AlertBanner/AlertBanner" -import { DockerIcon } from "components/Icons/DockerIcon" -import { MarkdownIcon } from "components/Icons/MarkdownIcon" -import { TerraformIcon } from "components/Icons/TerraformIcon" import { Loader } from "components/Loader/Loader" import { Margins } from "components/Margins/Margins" import { @@ -15,89 +11,14 @@ import { } from "components/PageHeader/PageHeader" import { Stack } from "components/Stack/Stack" import { Stats, StatsItem } from "components/Stats/Stats" -import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter" +import { TemplateFiles } from "components/TemplateFiles/TemplateFiles" import { UseTabResult } from "hooks/useTab" import { FC } from "react" import { useTranslation } from "react-i18next" import { Link as RouterLink } from "react-router-dom" -import { combineClasses } from "util/combineClasses" import { createDayString } from "util/createDayString" -import { TemplateVersionFiles } from "util/templateVersion" import { TemplateVersionMachineContext } from "xServices/templateVersion/templateVersionXService" -const iconByExtension: Record = { - tf: , - md: , - mkd: , - Dockerfile: , -} - -const getExtension = (filename: string) => { - if (filename.includes(".")) { - const [_, extension] = filename.split(".") - return extension - } - - return filename -} - -const languageByExtension: Record = { - tf: "hcl", - md: "markdown", - mkd: "markdown", - Dockerfile: "dockerfile", -} - -const Files: FC<{ - currentFiles: TemplateVersionFiles - previousFiles?: TemplateVersionFiles - tab: UseTabResult -}> = ({ currentFiles, previousFiles, tab }) => { - const styles = useStyles() - const filenames = Object.keys(currentFiles) - const selectedFilename = filenames[Number(tab.value)] - const currentFile = currentFiles[selectedFilename] - const previousFile = previousFiles && previousFiles[selectedFilename] - - return ( -
-
- {filenames.map((filename, index) => { - const tabValue = index.toString() - const extension = getExtension(filename) - const icon = iconByExtension[extension] - const hasDiff = - previousFiles && - previousFiles[filename] && - currentFiles[filename] !== previousFiles[filename] - - return ( - - ) - })} -
- - -
- ) -} export interface TemplateVersionPageViewProps { /** * Used to display the version name before loading the version in the API @@ -165,7 +86,7 @@ export const TemplateVersionPageView: FC = ({ /> - = ({ ) } -const useStyles = makeStyles((theme) => ({ - tabsWrapper: { - borderBottom: `1px solid ${theme.palette.divider}`, - }, - - tabs: { - display: "flex", - alignItems: "baseline", - borderBottom: `1px solid ${theme.palette.divider}`, - gap: 1, - }, - - tab: { - background: "transparent", - border: 0, - padding: theme.spacing(0, 3), - display: "flex", - alignItems: "center", - height: theme.spacing(6), - opacity: 0.85, - cursor: "pointer", - gap: theme.spacing(0.5), - position: "relative", - - "& svg": { - width: 22, - maxHeight: 16, - }, - - "&:hover": { - backgroundColor: theme.palette.action.hover, - }, - }, - - tabActive: { - opacity: 1, - background: theme.palette.action.hover, - - "&:after": { - content: '""', - display: "block", - height: 1, - width: "100%", - bottom: 0, - left: 0, - backgroundColor: theme.palette.secondary.dark, - position: "absolute", - }, - }, - - tabDiff: { - height: 6, - width: 6, - backgroundColor: theme.palette.warning.light, - borderRadius: "100%", - marginLeft: theme.spacing(0.5), - }, - - codeWrapper: { - background: theme.palette.background.paperLight, - }, - - files: { - borderRadius: theme.shape.borderRadius, - border: `1px solid ${theme.palette.divider}`, - }, - - prism: { - borderRadius: 0, - }, -})) - export default TemplateVersionPageView diff --git a/site/src/theme/overrides.ts b/site/src/theme/overrides.ts index 19b2947940ab2..e52681da016bb 100644 --- a/site/src/theme/overrides.ts +++ b/site/src/theme/overrides.ts @@ -234,5 +234,13 @@ export const getOverrides = ({ backgroundColor: colors.gray[12], }, }, + MuiMenu: { + paper: { + marginTop: 8, + borderRadius: 4, + padding: "4px 0", + minWidth: 120, + }, + }, } } diff --git a/site/src/xServices/template/templateXService.ts b/site/src/xServices/template/templateXService.ts deleted file mode 100644 index b7b836e4565ef..0000000000000 --- a/site/src/xServices/template/templateXService.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { assign, createMachine } from "xstate" -import { - checkAuthorization, - getTemplateByName, - getTemplateDAUs, - getTemplateVersion, - getTemplateVersionResources, - getTemplateVersions, -} from "api/api" -import { - AuthorizationResponse, - Template, - TemplateDAUsResponse, - TemplateVersion, - WorkspaceResource, -} from "api/typesGenerated" - -export interface TemplateContext { - organizationId: string - templateName: string - template?: Template - activeTemplateVersion?: TemplateVersion - templateResources?: WorkspaceResource[] - templateVersions?: TemplateVersion[] - templateDAUs?: TemplateDAUsResponse - permissions?: AuthorizationResponse - getTemplateError?: Error | unknown -} - -const getPermissionsToCheck = (templateId: string) => ({ - canUpdateTemplate: { - object: { - resource_type: "template", - resource_id: templateId, - }, - action: "update", - }, -}) - -export const templateMachine = - /** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOhgBdyCoAVMVABwBt1ywBiCAe0JIIDcuAazAk0WPIVIUq+WvWaswCAV0ytcPANoAGALqJQDLrFxUehkAA9EAJgDsOkgGZbO2wE5bANg8BGABZnHR0AgBoQAE9EPx0-Eg9EpIAOe28-D28ggF9siPEcAmI+fDNcdCYASXwAMy4SLCp+MDpGFjYANTAAJ1MeMjBKagBBTCaWhXawLt7NfE4eUVURMQxCqRKyiuq6hrHcZtbFTp6+-AGhuVHxo6mZs5V8QXVzfF0DJBBjU1fLGwQAgF4t4AKzeWzOTIhEEhZIRaIIZI6EEkEIhZyg2xuEF+XL5NaSYoELZVWr1NhtJQAJTgXAArt1MHALrJ5JS2DTYPTGXAFrxlqICoTSMSqNsySQKccwJzuUzYCzqLdqbSGfLHs8NNp9JZvmULJ9-kDkiRks4QR50SDkh5nH5nPCYjpXKi0SDnObbeaAniQEKiiLSmLSbspXdTnMFTIlZMlPdI3ylk9hIKCQHNsGduTYydZjwo4NWcrc2dYBq1Fq3jrPnrfobEAFQqbQt5kiCcf4go6ECD7Ca0XFMskAmksr7-RtReUQ1xEyRYOQlKsJOmp+K6rqTPr8H9ELaTclkg5wQEzRidB5u-Y0q6QgFbAEsuCMuO0xsmFx0BBIOwACIAUQAGX-Gh-03H45ksBFrycGE7zcW1bD8e0In+Pw3G8EggT8M0-Fbc8PGSV8Vw2TAeBqXBulQahfzAJhBg4ABhAB5AA5AAxSoqQAWQAfQA4DQPA7ddwQZCMVRUF7Bw3svXsEFuz8MFMNtC8bRtHQzWHYj1mKMjako6i5Fo+i2HYRjhlYxigP4oCQLAmstzrUA0OcaSSHsZwbSUjwAl83zwiiGJGw8LCwTtU8vGQnI8j9N9im-UzqDnAUSEShjizAYTnOsRB7BHEglLwoqskCWxFJ8ewPJ0bx8tsC1IQhZwdOFNK6MGZKem6LhuhIY46iotrTImdkssciCDRcvcbVRJrwr8fLXHsRTYRIOCau8WqYRxIjfXwLhv3gT4J2KaM5Ey7LIPrAFyqChBLVvEJPGk2w21PFrVyDacsz2G4c2mCN+jOqBrgOEbpXjSavicq6prEhar1tR7IXSaSfVik7AxJH7GjBzLIfOWA6UweUjqMGGof+TzbEKsEMiRdwYUhbtkhw5HnHvXx0g8D7Jy+9d6lxw5-oJy7Kb3B07vsJDkekjwcSyWxeaJfmZ0lf7ZTVZlgcyzWeTJ6GJp3a7kOWu7YjcR6wQCEFAQfbxlaxzMJTDFUuS1hUiZJuADdrWHcoQVtMJxdtWfvaL0hWm2rfcRXHxBR2M2+l2NdVfWxeNuHbW7Xz+xCWJkm8ZE3LbRO1zV12S0jRVzpFwH8F9inM4D03u2Ux6sWvQj2wTjH4qd5PQzrvMG-nYnSYz0TQRNG3gnUyE+zNNv-EevDrUya9mr7kiVexlPRoJxujdE7O7r8gIO+dXtUkyMvVazSfrt8bt7xpgcFttC1nXsROPy-SBH5w1iO6MKwRghAkbH2BSUtHBrTRPeC8rhxKJ30hRKiNF2psEAS3ZCNMkR4R8MOWqfloEIntCvDmnoRzyUIvLRO6VWTYP+F4TCNt0g22REhV6pCYgEJIPVDENsPBpA9GkehmCAHjREtdVmmFPCKy2u6RwcJzaQicMOb0wQ3ALVxNvXSRAmExBUWQvOA5baqXlrbXIuQgA */ - createMachine( - { - id: "templateMachine", - predictableActionArguments: true, - tsTypes: {} as import("./templateXService.typegen").Typegen0, - schema: { - context: {} as TemplateContext, - services: {} as { - getTemplate: { - data: Template - } - getActiveTemplateVersion: { - data: TemplateVersion - } - getTemplateResources: { - data: WorkspaceResource[] - } - getTemplateVersions: { - data: TemplateVersion[] - } - getTemplateDAUs: { - data: TemplateDAUsResponse - } - getTemplatePermissions: { - data: AuthorizationResponse - } - }, - }, - initial: "gettingTemplate", - states: { - gettingTemplate: { - invoke: { - src: "getTemplate", - onDone: [ - { - actions: "assignTemplate", - target: "initialInfo", - }, - ], - onError: [ - { - actions: "assignGetTemplateError", - target: "error", - }, - ], - }, - }, - initialInfo: { - type: "parallel", - states: { - activeTemplateVersion: { - initial: "gettingActiveTemplateVersion", - states: { - gettingActiveTemplateVersion: { - invoke: { - src: "getActiveTemplateVersion", - onDone: [ - { - actions: "assignActiveTemplateVersion", - target: "success", - }, - ], - }, - }, - success: { - type: "final", - }, - }, - }, - templateResources: { - initial: "gettingTemplateResources", - states: { - gettingTemplateResources: { - invoke: { - src: "getTemplateResources", - onDone: [ - { - actions: "assignTemplateResources", - target: "success", - }, - ], - }, - }, - success: { - type: "final", - }, - }, - }, - templateVersions: { - initial: "gettingTemplateVersions", - states: { - gettingTemplateVersions: { - invoke: { - src: "getTemplateVersions", - onDone: [ - { - actions: "assignTemplateVersions", - target: "success", - }, - ], - }, - }, - success: { - type: "final", - }, - }, - }, - templateDAUs: { - initial: "gettingTemplateDAUs", - states: { - gettingTemplateDAUs: { - invoke: { - src: "getTemplateDAUs", - onDone: [ - { - actions: "assignTemplateDAUs", - target: "success", - }, - ], - }, - }, - success: { - type: "final", - }, - }, - }, - templatePermissions: { - initial: "gettingTemplatePermissions", - states: { - gettingTemplatePermissions: { - invoke: { - src: "getTemplatePermissions", - onDone: { - target: "success", - actions: "assignPermissions", - }, - }, - }, - success: { - type: "final", - }, - }, - }, - }, - onDone: { - target: "loaded", - }, - }, - loaded: { - initial: "waiting", - states: { - refreshingTemplate: { - invoke: { - id: "refreshTemplate", - src: "getTemplate", - onDone: { target: "waiting", actions: "assignTemplate" }, - }, - }, - waiting: { - after: { - 5000: "refreshingTemplate", - }, - }, - }, - }, - error: { - type: "final", - }, - }, - }, - { - services: { - getTemplate: (ctx) => - getTemplateByName(ctx.organizationId, ctx.templateName), - getActiveTemplateVersion: (ctx) => { - if (!ctx.template) { - throw new Error("Template not loaded") - } - - return getTemplateVersion(ctx.template.active_version_id) - }, - getTemplateResources: (ctx) => { - if (!ctx.template) { - throw new Error("Template not loaded") - } - - return getTemplateVersionResources(ctx.template.active_version_id) - }, - getTemplateVersions: (ctx) => { - if (!ctx.template) { - throw new Error("Template not loaded") - } - - return getTemplateVersions(ctx.template.id) - }, - getTemplateDAUs: (ctx) => { - if (!ctx.template) { - throw new Error("Template not loaded") - } - return getTemplateDAUs(ctx.template.id) - }, - getTemplatePermissions: (ctx) => { - if (!ctx.template) { - throw new Error("Template not loaded") - } - return checkAuthorization({ - checks: getPermissionsToCheck(ctx.template.id), - }) - }, - }, - actions: { - assignTemplate: assign({ - template: (_, event) => event.data, - }), - assignActiveTemplateVersion: assign({ - activeTemplateVersion: (_, event) => event.data, - }), - assignGetTemplateError: assign({ - getTemplateError: (_, event) => event.data, - }), - assignTemplateResources: assign({ - templateResources: (_, event) => event.data, - }), - assignTemplateVersions: assign({ - templateVersions: (_, event) => event.data, - }), - assignTemplateDAUs: assign({ - templateDAUs: (_, event) => event.data, - }), - assignPermissions: assign({ - permissions: (_, event) => event.data, - }), - }, - }, - )