mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): Add source code tab on template page, group buttons and add edit file option (#6681)
This commit is contained in:
@@ -125,10 +125,12 @@ const TemplateVariablesPage = lazy(
|
|||||||
const WorkspaceSettingsPage = lazy(
|
const WorkspaceSettingsPage = lazy(
|
||||||
() => import("./pages/WorkspaceSettingsPage/WorkspaceSettingsPage"),
|
() => import("./pages/WorkspaceSettingsPage/WorkspaceSettingsPage"),
|
||||||
)
|
)
|
||||||
|
|
||||||
const CreateTokenPage = lazy(
|
const CreateTokenPage = lazy(
|
||||||
() => import("./pages/CreateTokenPage/CreateTokenPage"),
|
() => import("./pages/CreateTokenPage/CreateTokenPage"),
|
||||||
)
|
)
|
||||||
|
const TemplateFilesPage = lazy(
|
||||||
|
() => import("./pages/TemplateFilesPage/TemplateFilesPage"),
|
||||||
|
)
|
||||||
|
|
||||||
export const AppRouter: FC = () => {
|
export const AppRouter: FC = () => {
|
||||||
return (
|
return (
|
||||||
@@ -162,6 +164,7 @@ export const AppRouter: FC = () => {
|
|||||||
path="permissions"
|
path="permissions"
|
||||||
element={<TemplatePermissionsPage />}
|
element={<TemplatePermissionsPage />}
|
||||||
/>
|
/>
|
||||||
|
<Route path="files" element={<TemplateFilesPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="workspace" element={<CreateWorkspacePage />} />
|
<Route path="workspace" element={<CreateWorkspacePage />} />
|
||||||
|
|||||||
@@ -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<string, JSX.Element> = {
|
||||||
|
tf: <TerraformIcon />,
|
||||||
|
md: <MarkdownIcon />,
|
||||||
|
mkd: <MarkdownIcon />,
|
||||||
|
Dockerfile: <DockerIcon />,
|
||||||
|
}
|
||||||
|
|
||||||
|
const getExtension = (filename: string) => {
|
||||||
|
if (filename.includes(".")) {
|
||||||
|
const [_, extension] = filename.split(".")
|
||||||
|
return extension
|
||||||
|
}
|
||||||
|
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
|
||||||
|
const languageByExtension: Record<string, string> = {
|
||||||
|
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 (
|
||||||
|
<div className={styles.files}>
|
||||||
|
<div className={styles.tabs}>
|
||||||
|
{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 (
|
||||||
|
<button
|
||||||
|
className={combineClasses({
|
||||||
|
[styles.tab]: true,
|
||||||
|
[styles.tabActive]: tabValue === tab.value,
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
tab.set(tabValue)
|
||||||
|
}}
|
||||||
|
key={filename}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{filename}
|
||||||
|
{hasDiff && <div className={styles.tabDiff} />}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SyntaxHighlighter
|
||||||
|
value={currentFile}
|
||||||
|
compareWith={previousFile}
|
||||||
|
language={languageByExtension[getExtension(selectedFilename)]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}))
|
||||||
@@ -1,36 +1,56 @@
|
|||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import { useMachine } from "@xstate/react"
|
|
||||||
import { useOrganizationId } from "hooks/useOrganizationId"
|
import { useOrganizationId } from "hooks/useOrganizationId"
|
||||||
import { createContext, FC, Suspense, useContext } from "react"
|
import { createContext, FC, Suspense, useContext } from "react"
|
||||||
import { NavLink, Outlet, useNavigate, useParams } from "react-router-dom"
|
import { NavLink, Outlet, useNavigate, useParams } from "react-router-dom"
|
||||||
import { combineClasses } from "util/combineClasses"
|
import { combineClasses } from "util/combineClasses"
|
||||||
import {
|
|
||||||
TemplateContext,
|
|
||||||
templateMachine,
|
|
||||||
} from "xServices/template/templateXService"
|
|
||||||
import { Margins } from "components/Margins/Margins"
|
import { Margins } from "components/Margins/Margins"
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
import { Permissions } from "xServices/auth/authXService"
|
|
||||||
import { Loader } from "components/Loader/Loader"
|
import { Loader } from "components/Loader/Loader"
|
||||||
import { usePermissions } from "hooks/usePermissions"
|
|
||||||
import { TemplatePageHeader } from "./TemplatePageHeader"
|
import { TemplatePageHeader } from "./TemplatePageHeader"
|
||||||
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
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 useTemplateName = () => {
|
const templatePermissions = (templateId: string) => ({
|
||||||
const { template } = useParams()
|
canUpdateTemplate: {
|
||||||
|
object: {
|
||||||
|
resource_type: "template",
|
||||||
|
resource_id: templateId,
|
||||||
|
},
|
||||||
|
action: "update",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
if (!template) {
|
const fetchTemplate = async (orgId: string, templateName: string) => {
|
||||||
throw new Error("No template found in the URL")
|
const template = await getTemplateByName(orgId, templateName)
|
||||||
|
const [activeVersion, permissions] = await Promise.all([
|
||||||
|
getTemplateVersion(template.active_version_id),
|
||||||
|
checkAuthorization({
|
||||||
|
checks: templatePermissions(template.id),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
template,
|
||||||
|
activeVersion,
|
||||||
|
permissions,
|
||||||
}
|
}
|
||||||
|
|
||||||
return template
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TemplateLayoutContextValue = {
|
const useTemplateData = (orgId: string, templateName: string) => {
|
||||||
context: TemplateContext
|
return useQuery({
|
||||||
permissions?: Permissions
|
queryKey: ["template", templateName],
|
||||||
|
queryFn: () => fetchTemplate(orgId, templateName),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TemplateLayoutContextValue = Awaited<ReturnType<typeof fetchTemplate>>
|
||||||
|
|
||||||
const TemplateLayoutContext = createContext<
|
const TemplateLayoutContext = createContext<
|
||||||
TemplateLayoutContextValue | undefined
|
TemplateLayoutContextValue | undefined
|
||||||
>(undefined)
|
>(undefined)
|
||||||
@@ -50,38 +70,30 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const organizationId = useOrganizationId()
|
const orgId = useOrganizationId()
|
||||||
const templateName = useTemplateName()
|
const { template } = useParams() as { template: string }
|
||||||
const [templateState, _] = useMachine(templateMachine, {
|
const templateData = useTemplateData(orgId, template)
|
||||||
context: {
|
const dashboard = useDashboard()
|
||||||
templateName,
|
|
||||||
organizationId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const {
|
|
||||||
template,
|
|
||||||
permissions: templatePermissions,
|
|
||||||
getTemplateError,
|
|
||||||
} = templateState.context
|
|
||||||
const permissions = usePermissions()
|
|
||||||
|
|
||||||
if (getTemplateError) {
|
if (templateData.error) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.error}>
|
<div className={styles.error}>
|
||||||
<AlertBanner severity="error" error={getTemplateError} />
|
<AlertBanner severity="error" error={templateData.error} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!template || !templatePermissions) {
|
if (templateData.isLoading || !templateData.data) {
|
||||||
return <Loader />
|
return <Loader />
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TemplatePageHeader
|
<TemplatePageHeader
|
||||||
template={template}
|
template={templateData.data.template}
|
||||||
permissions={templatePermissions}
|
activeVersion={templateData.data.activeVersion}
|
||||||
|
permissions={templateData.data.permissions}
|
||||||
|
canEditFiles={dashboard.experiments.includes("template_editor")}
|
||||||
onDeleteTemplate={() => {
|
onDeleteTemplate={() => {
|
||||||
navigate("/templates")
|
navigate("/templates")
|
||||||
}}
|
}}
|
||||||
@@ -92,7 +104,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
|
|||||||
<Stack direction="row" spacing={0.25}>
|
<Stack direction="row" spacing={0.25}>
|
||||||
<NavLink
|
<NavLink
|
||||||
end
|
end
|
||||||
to={`/templates/${template.name}`}
|
to={`/templates/${template}`}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
combineClasses([
|
combineClasses([
|
||||||
styles.tabItem,
|
styles.tabItem,
|
||||||
@@ -103,7 +115,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
|
|||||||
Summary
|
Summary
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink
|
<NavLink
|
||||||
to={`/templates/${template.name}/permissions`}
|
to={`/templates/${template}/permissions`}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
combineClasses([
|
combineClasses([
|
||||||
styles.tabItem,
|
styles.tabItem,
|
||||||
@@ -113,14 +125,23 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
|
|||||||
>
|
>
|
||||||
Permissions
|
Permissions
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
to={`/templates/${template}/files`}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
combineClasses([
|
||||||
|
styles.tabItem,
|
||||||
|
isActive ? styles.tabItemActive : undefined,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Source Code
|
||||||
|
</NavLink>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Margins>
|
</Margins>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Margins>
|
<Margins>
|
||||||
<TemplateLayoutContext.Provider
|
<TemplateLayoutContext.Provider value={templateData.data}>
|
||||||
value={{ permissions, context: templateState.context }}
|
|
||||||
>
|
|
||||||
<Suspense fallback={<Loader />}>{children}</Suspense>
|
<Suspense fallback={<Loader />}>{children}</Suspense>
|
||||||
</TemplateLayoutContext.Provider>
|
</TemplateLayoutContext.Provider>
|
||||||
</Margins>
|
</Margins>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ComponentMeta, Story } from "@storybook/react"
|
import { ComponentMeta, Story } from "@storybook/react"
|
||||||
import { MockTemplate } from "testHelpers/entities"
|
import { MockTemplate, MockTemplateVersion } from "testHelpers/entities"
|
||||||
import {
|
import {
|
||||||
TemplatePageHeader,
|
TemplatePageHeader,
|
||||||
TemplatePageHeaderProps,
|
TemplatePageHeaderProps,
|
||||||
@@ -12,6 +12,9 @@ export default {
|
|||||||
template: {
|
template: {
|
||||||
defaultValue: MockTemplate,
|
defaultValue: MockTemplate,
|
||||||
},
|
},
|
||||||
|
activeVersion: {
|
||||||
|
defaultValue: MockTemplateVersion,
|
||||||
|
},
|
||||||
permissions: {
|
permissions: {
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
canUpdateTemplate: true,
|
canUpdateTemplate: true,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import Button from "@material-ui/core/Button"
|
import Button from "@material-ui/core/Button"
|
||||||
import DeleteOutlined from "@material-ui/icons/DeleteOutlined"
|
|
||||||
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
|
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
|
||||||
import SettingsOutlined from "@material-ui/icons/SettingsOutlined"
|
import {
|
||||||
import CodeOutlined from "@material-ui/icons/CodeOutlined"
|
AuthorizationResponse,
|
||||||
import { AuthorizationResponse, Template } from "api/typesGenerated"
|
Template,
|
||||||
|
TemplateVersion,
|
||||||
|
} from "api/typesGenerated"
|
||||||
import { Avatar } from "components/Avatar/Avatar"
|
import { Avatar } from "components/Avatar/Avatar"
|
||||||
import { Maybe } from "components/Conditionals/Maybe"
|
import { Maybe } from "components/Conditionals/Maybe"
|
||||||
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"
|
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"
|
||||||
@@ -13,44 +14,87 @@ import {
|
|||||||
PageHeaderSubtitle,
|
PageHeaderSubtitle,
|
||||||
} from "components/PageHeader/PageHeader"
|
} from "components/PageHeader/PageHeader"
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
import { FC } from "react"
|
import { FC, useState } from "react"
|
||||||
import { Link as RouterLink } from "react-router-dom"
|
import { Link as RouterLink } from "react-router-dom"
|
||||||
import { useDeleteTemplate } from "./deleteTemplate"
|
import { useDeleteTemplate } from "./deleteTemplate"
|
||||||
import { Margins } from "components/Margins/Margins"
|
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 = {
|
const Language = {
|
||||||
editButton: "Edit",
|
|
||||||
variablesButton: "Variables",
|
variablesButton: "Variables",
|
||||||
settingsButton: "Settings",
|
settingsButton: "Settings",
|
||||||
createButton: "Create workspace",
|
createButton: "Create workspace",
|
||||||
deleteButton: "Delete",
|
deleteButton: "Delete",
|
||||||
|
editFilesButton: "Edit files",
|
||||||
}
|
}
|
||||||
|
|
||||||
const TemplateSettingsButton: FC<{ templateName: string }> = ({
|
const TemplateMenu: FC<{
|
||||||
templateName,
|
templateName: string
|
||||||
}) => (
|
templateVersion: string
|
||||||
<Button
|
canEditFiles: boolean
|
||||||
variant="outlined"
|
onDelete: () => void
|
||||||
component={RouterLink}
|
}> = ({ templateName, templateVersion, canEditFiles, onDelete }) => {
|
||||||
to={`/templates/${templateName}/settings`}
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null)
|
||||||
startIcon={<SettingsOutlined />}
|
|
||||||
>
|
|
||||||
{Language.settingsButton}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
|
|
||||||
const TemplateVariablesButton: FC<{ templateName: string }> = ({
|
const handleClose = () => {
|
||||||
templateName,
|
setAnchorEl(null)
|
||||||
}) => (
|
}
|
||||||
<Button
|
|
||||||
variant="outlined"
|
return (
|
||||||
component={RouterLink}
|
<div>
|
||||||
to={`/templates/${templateName}/variables`}
|
<Button
|
||||||
startIcon={<CodeOutlined />}
|
variant="outlined"
|
||||||
>
|
aria-controls="template-options"
|
||||||
{Language.variablesButton}
|
aria-haspopup="true"
|
||||||
</Button>
|
onClick={(e) => setAnchorEl(e.currentTarget)}
|
||||||
)
|
>
|
||||||
|
<MoreVertOutlined />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
id="template-options"
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
keepMounted
|
||||||
|
open={Boolean(anchorEl)}
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleClose}
|
||||||
|
component={RouterLink}
|
||||||
|
to={`/templates/${templateName}/settings`}
|
||||||
|
>
|
||||||
|
{Language.settingsButton}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
component={RouterLink}
|
||||||
|
to={`/templates/${templateName}/variables`}
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
{Language.variablesButton}
|
||||||
|
</MenuItem>
|
||||||
|
{canEditFiles && (
|
||||||
|
<MenuItem
|
||||||
|
component={RouterLink}
|
||||||
|
to={`/templates/${templateName}/versions/${templateVersion}/edit`}
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
{Language.editFilesButton}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
onDelete()
|
||||||
|
handleClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Language.deleteButton}
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const CreateWorkspaceButton: FC<{
|
const CreateWorkspaceButton: FC<{
|
||||||
templateName: string
|
templateName: string
|
||||||
@@ -65,21 +109,19 @@ const CreateWorkspaceButton: FC<{
|
|||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
|
|
||||||
const DeleteTemplateButton: FC<{ onClick: () => void }> = ({ onClick }) => (
|
|
||||||
<Button variant="outlined" startIcon={<DeleteOutlined />} onClick={onClick}>
|
|
||||||
{Language.deleteButton}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
|
|
||||||
export type TemplatePageHeaderProps = {
|
export type TemplatePageHeaderProps = {
|
||||||
template: Template
|
template: Template
|
||||||
|
activeVersion: TemplateVersion
|
||||||
permissions: AuthorizationResponse
|
permissions: AuthorizationResponse
|
||||||
|
canEditFiles: boolean
|
||||||
onDeleteTemplate: () => void
|
onDeleteTemplate: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplatePageHeader: FC<TemplatePageHeaderProps> = ({
|
export const TemplatePageHeader: FC<TemplatePageHeaderProps> = ({
|
||||||
template,
|
template,
|
||||||
|
activeVersion,
|
||||||
permissions,
|
permissions,
|
||||||
|
canEditFiles,
|
||||||
onDeleteTemplate,
|
onDeleteTemplate,
|
||||||
}) => {
|
}) => {
|
||||||
const hasIcon = template.icon && template.icon !== ""
|
const hasIcon = template.icon && template.icon !== ""
|
||||||
@@ -90,14 +132,15 @@ export const TemplatePageHeader: FC<TemplatePageHeaderProps> = ({
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
<Maybe condition={permissions.canUpdateTemplate}>
|
|
||||||
<DeleteTemplateButton
|
|
||||||
onClick={deleteTemplate.openDeleteConfirmation}
|
|
||||||
/>
|
|
||||||
<TemplateSettingsButton templateName={template.name} />
|
|
||||||
<TemplateVariablesButton templateName={template.name} />
|
|
||||||
</Maybe>
|
|
||||||
<CreateWorkspaceButton templateName={template.name} />
|
<CreateWorkspaceButton templateName={template.name} />
|
||||||
|
<Maybe condition={permissions.canUpdateTemplate}>
|
||||||
|
<TemplateMenu
|
||||||
|
templateVersion={activeVersion.name}
|
||||||
|
templateName={template.name}
|
||||||
|
onDelete={deleteTemplate.openDeleteConfirmation}
|
||||||
|
canEditFiles={canEditFiles}
|
||||||
|
/>
|
||||||
|
</Maybe>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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<typeof getTemplateVersionFiles>[] = []
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>{pageTitle(`${template?.name} · Source Code`)}</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
{templateFiles ? (
|
||||||
|
<TemplateFiles
|
||||||
|
currentFiles={templateFiles.currentFiles}
|
||||||
|
previousFiles={templateFiles.previousFiles}
|
||||||
|
tab={tab}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Loader />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TemplateFilesPage
|
||||||
@@ -18,11 +18,10 @@ export const TemplatePermissionsPage: FC<
|
|||||||
React.PropsWithChildren<unknown>
|
React.PropsWithChildren<unknown>
|
||||||
> = () => {
|
> = () => {
|
||||||
const organizationId = useOrganizationId()
|
const organizationId = useOrganizationId()
|
||||||
const { context } = useTemplateLayoutContext()
|
const { template, permissions } = useTemplateLayoutContext()
|
||||||
const { template, permissions } = context
|
|
||||||
const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility()
|
const { template_rbac: isTemplateRBACEnabled } = useFeatureVisibility()
|
||||||
const [state, send] = useMachine(templateACLMachine, {
|
const [state, send] = useMachine(templateACLMachine, {
|
||||||
context: { templateId: template?.id },
|
context: { templateId: template.id },
|
||||||
})
|
})
|
||||||
const { templateACL, userToBeUpdated, groupToBeUpdated } = state.context
|
const { templateACL, userToBeUpdated, groupToBeUpdated } = state.context
|
||||||
|
|
||||||
|
|||||||
@@ -2,22 +2,15 @@ import { useTemplateLayoutContext } from "components/TemplateLayout/TemplateLayo
|
|||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { Helmet } from "react-helmet-async"
|
import { Helmet } from "react-helmet-async"
|
||||||
import { pageTitle } from "util/page"
|
import { pageTitle } from "util/page"
|
||||||
|
import { useTemplateSummaryData } from "./data"
|
||||||
import { TemplateSummaryPageView } from "./TemplateSummaryPageView"
|
import { TemplateSummaryPageView } from "./TemplateSummaryPageView"
|
||||||
import { Loader } from "components/Loader/Loader"
|
|
||||||
|
|
||||||
export const TemplateSummaryPage: FC = () => {
|
export const TemplateSummaryPage: FC = () => {
|
||||||
const { context } = useTemplateLayoutContext()
|
const { template, activeVersion } = useTemplateLayoutContext()
|
||||||
const {
|
const { data } = useTemplateSummaryData(
|
||||||
template,
|
template.id,
|
||||||
activeTemplateVersion,
|
template.active_version_id,
|
||||||
templateResources,
|
)
|
||||||
templateVersions,
|
|
||||||
templateDAUs,
|
|
||||||
} = context
|
|
||||||
|
|
||||||
if (!template || !activeTemplateVersion || !templateResources) {
|
|
||||||
return <Loader />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -33,11 +26,9 @@ export const TemplateSummaryPage: FC = () => {
|
|||||||
</title>
|
</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<TemplateSummaryPageView
|
<TemplateSummaryPageView
|
||||||
|
data={data}
|
||||||
template={template}
|
template={template}
|
||||||
activeTemplateVersion={activeTemplateVersion}
|
activeVersion={activeVersion}
|
||||||
templateResources={templateResources}
|
|
||||||
templateVersions={templateVersions}
|
|
||||||
templateDAUs={templateDAUs}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
+27
-27
@@ -17,47 +17,47 @@ const Template: Story<TemplateSummaryPageViewProps> = (args) => (
|
|||||||
export const Example = Template.bind({})
|
export const Example = Template.bind({})
|
||||||
Example.args = {
|
Example.args = {
|
||||||
template: Mocks.MockTemplate,
|
template: Mocks.MockTemplate,
|
||||||
activeTemplateVersion: Mocks.MockTemplateVersion,
|
activeVersion: Mocks.MockTemplateVersion,
|
||||||
templateResources: [
|
data: {
|
||||||
Mocks.MockWorkspaceResource,
|
resources: [Mocks.MockWorkspaceResource, Mocks.MockWorkspaceResource2],
|
||||||
Mocks.MockWorkspaceResource2,
|
versions: [Mocks.MockTemplateVersion],
|
||||||
],
|
daus: Mocks.MockTemplateDAUResponse,
|
||||||
templateVersions: [Mocks.MockTemplateVersion],
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NoIcon = Template.bind({})
|
export const NoIcon = Template.bind({})
|
||||||
NoIcon.args = {
|
NoIcon.args = {
|
||||||
template: { ...Mocks.MockTemplate, icon: "" },
|
template: { ...Mocks.MockTemplate, icon: "" },
|
||||||
activeTemplateVersion: Mocks.MockTemplateVersion,
|
activeVersion: Mocks.MockTemplateVersion,
|
||||||
templateResources: [
|
data: {
|
||||||
Mocks.MockWorkspaceResource,
|
resources: [Mocks.MockWorkspaceResource, Mocks.MockWorkspaceResource2],
|
||||||
Mocks.MockWorkspaceResource2,
|
versions: [Mocks.MockTemplateVersion],
|
||||||
],
|
daus: Mocks.MockTemplateDAUResponse,
|
||||||
templateVersions: [Mocks.MockTemplateVersion],
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SmallViewport = Template.bind({})
|
export const SmallViewport = Template.bind({})
|
||||||
SmallViewport.args = {
|
SmallViewport.args = {
|
||||||
template: Mocks.MockTemplate,
|
template: Mocks.MockTemplate,
|
||||||
activeTemplateVersion: {
|
activeVersion: {
|
||||||
...Mocks.MockTemplateVersion,
|
...Mocks.MockTemplateVersion,
|
||||||
readme: `---
|
readme: `---
|
||||||
name:Template test
|
name:Template test
|
||||||
---
|
---
|
||||||
## Instructions
|
## Instructions
|
||||||
You can add instructions here
|
You can add instructions here
|
||||||
|
|
||||||
[Some link info](https://coder.com)
|
[Some link info](https://coder.com)
|
||||||
\`\`\`
|
\`\`\`
|
||||||
# This is a really long sentence to test that the code block wraps into a new line properly.
|
# 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 = {
|
SmallViewport.parameters = {
|
||||||
chromatic: { viewports: [600] },
|
chromatic: { viewports: [600] },
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import {
|
import {
|
||||||
Template,
|
Template,
|
||||||
TemplateDAUsResponse,
|
|
||||||
TemplateVersion,
|
TemplateVersion,
|
||||||
WorkspaceResource,
|
WorkspaceResource,
|
||||||
} from "api/typesGenerated"
|
} from "api/typesGenerated"
|
||||||
|
import { Loader } from "components/Loader/Loader"
|
||||||
import { MemoizedMarkdown } from "components/Markdown/Markdown"
|
import { MemoizedMarkdown } from "components/Markdown/Markdown"
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable"
|
import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable"
|
||||||
@@ -13,26 +13,27 @@ import { VersionsTable } from "components/VersionsTable/VersionsTable"
|
|||||||
import frontMatter from "front-matter"
|
import frontMatter from "front-matter"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { DAUChart } from "../../../components/DAUChart/DAUChart"
|
import { DAUChart } from "../../../components/DAUChart/DAUChart"
|
||||||
|
import { TemplateSummaryData } from "./data"
|
||||||
|
|
||||||
export interface TemplateSummaryPageViewProps {
|
export interface TemplateSummaryPageViewProps {
|
||||||
|
data?: TemplateSummaryData
|
||||||
template: Template
|
template: Template
|
||||||
activeTemplateVersion: TemplateVersion
|
activeVersion: TemplateVersion
|
||||||
templateResources: WorkspaceResource[]
|
|
||||||
templateVersions?: TemplateVersion[]
|
|
||||||
templateDAUs?: TemplateDAUsResponse
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TemplateSummaryPageView: FC<
|
export const TemplateSummaryPageView: FC<TemplateSummaryPageViewProps> = ({
|
||||||
React.PropsWithChildren<TemplateSummaryPageViewProps>
|
data,
|
||||||
> = ({
|
|
||||||
template,
|
template,
|
||||||
activeTemplateVersion,
|
activeVersion,
|
||||||
templateResources,
|
|
||||||
templateVersions,
|
|
||||||
templateDAUs,
|
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const readme = frontMatter(activeTemplateVersion.readme)
|
|
||||||
|
if (!data) {
|
||||||
|
return <Loader />
|
||||||
|
}
|
||||||
|
|
||||||
|
const { daus, resources, versions } = data
|
||||||
|
const readme = frontMatter(activeVersion.readme)
|
||||||
|
|
||||||
const getStartedResources = (resources: WorkspaceResource[]) => {
|
const getStartedResources = (resources: WorkspaceResource[]) => {
|
||||||
return resources.filter(
|
return resources.filter(
|
||||||
@@ -42,14 +43,9 @@ export const TemplateSummaryPageView: FC<
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={4}>
|
<Stack spacing={4}>
|
||||||
<TemplateStats
|
<TemplateStats template={template} activeVersion={activeVersion} />
|
||||||
template={template}
|
{daus && <DAUChart daus={daus} />}
|
||||||
activeVersion={activeTemplateVersion}
|
<TemplateResourcesTable resources={getStartedResources(resources)} />
|
||||||
/>
|
|
||||||
{templateDAUs && <DAUChart daus={templateDAUs} />}
|
|
||||||
<TemplateResourcesTable
|
|
||||||
resources={getStartedResources(templateResources)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.markdownSection} id="readme">
|
<div className={styles.markdownSection} id="readme">
|
||||||
<div className={styles.readmeLabel}>README.md</div>
|
<div className={styles.readmeLabel}>README.md</div>
|
||||||
@@ -58,7 +54,7 @@ export const TemplateSummaryPageView: FC<
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<VersionsTable versions={templateVersions} />
|
<VersionsTable versions={versions} />
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<typeof fetchTemplateSummary>
|
||||||
|
>
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
import Button from "@material-ui/core/Button"
|
import Button from "@material-ui/core/Button"
|
||||||
import Link from "@material-ui/core/Link"
|
import Link from "@material-ui/core/Link"
|
||||||
import { makeStyles } from "@material-ui/core/styles"
|
|
||||||
import EditIcon from "@material-ui/icons/Edit"
|
import EditIcon from "@material-ui/icons/Edit"
|
||||||
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
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 { Loader } from "components/Loader/Loader"
|
||||||
import { Margins } from "components/Margins/Margins"
|
import { Margins } from "components/Margins/Margins"
|
||||||
import {
|
import {
|
||||||
@@ -15,89 +11,14 @@ import {
|
|||||||
} from "components/PageHeader/PageHeader"
|
} from "components/PageHeader/PageHeader"
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
import { Stats, StatsItem } from "components/Stats/Stats"
|
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 { UseTabResult } from "hooks/useTab"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link as RouterLink } from "react-router-dom"
|
import { Link as RouterLink } from "react-router-dom"
|
||||||
import { combineClasses } from "util/combineClasses"
|
|
||||||
import { createDayString } from "util/createDayString"
|
import { createDayString } from "util/createDayString"
|
||||||
import { TemplateVersionFiles } from "util/templateVersion"
|
|
||||||
import { TemplateVersionMachineContext } from "xServices/templateVersion/templateVersionXService"
|
import { TemplateVersionMachineContext } from "xServices/templateVersion/templateVersionXService"
|
||||||
|
|
||||||
const iconByExtension: Record<string, JSX.Element> = {
|
|
||||||
tf: <TerraformIcon />,
|
|
||||||
md: <MarkdownIcon />,
|
|
||||||
mkd: <MarkdownIcon />,
|
|
||||||
Dockerfile: <DockerIcon />,
|
|
||||||
}
|
|
||||||
|
|
||||||
const getExtension = (filename: string) => {
|
|
||||||
if (filename.includes(".")) {
|
|
||||||
const [_, extension] = filename.split(".")
|
|
||||||
return extension
|
|
||||||
}
|
|
||||||
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
|
|
||||||
const languageByExtension: Record<string, string> = {
|
|
||||||
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 (
|
|
||||||
<div className={styles.files}>
|
|
||||||
<div className={styles.tabs}>
|
|
||||||
{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 (
|
|
||||||
<button
|
|
||||||
className={combineClasses({
|
|
||||||
[styles.tab]: true,
|
|
||||||
[styles.tabActive]: tabValue === tab.value,
|
|
||||||
})}
|
|
||||||
onClick={() => {
|
|
||||||
tab.set(tabValue)
|
|
||||||
}}
|
|
||||||
key={filename}
|
|
||||||
>
|
|
||||||
{icon}
|
|
||||||
{filename}
|
|
||||||
{hasDiff && <div className={styles.tabDiff} />}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SyntaxHighlighter
|
|
||||||
value={currentFile}
|
|
||||||
compareWith={previousFile}
|
|
||||||
language={languageByExtension[getExtension(selectedFilename)]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export interface TemplateVersionPageViewProps {
|
export interface TemplateVersionPageViewProps {
|
||||||
/**
|
/**
|
||||||
* Used to display the version name before loading the version in the API
|
* Used to display the version name before loading the version in the API
|
||||||
@@ -165,7 +86,7 @@ export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
|
|||||||
/>
|
/>
|
||||||
</Stats>
|
</Stats>
|
||||||
|
|
||||||
<Files
|
<TemplateFiles
|
||||||
tab={tab}
|
tab={tab}
|
||||||
currentFiles={currentFiles}
|
currentFiles={currentFiles}
|
||||||
previousFiles={previousFiles}
|
previousFiles={previousFiles}
|
||||||
@@ -177,76 +98,4 @@ export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
export default TemplateVersionPageView
|
||||||
|
|||||||
@@ -234,5 +234,13 @@ export const getOverrides = ({
|
|||||||
backgroundColor: colors.gray[12],
|
backgroundColor: colors.gray[12],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
MuiMenu: {
|
||||||
|
paper: {
|
||||||
|
marginTop: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
padding: "4px 0",
|
||||||
|
minWidth: 120,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
Reference in New Issue
Block a user