mirror of
https://github.com/coder/coder.git
synced 2026-09-22 05:05:20 +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(
|
||||
() => 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={<TemplatePermissionsPage />}
|
||||
/>
|
||||
<Route path="files" element={<TemplateFilesPage />} />
|
||||
</Route>
|
||||
|
||||
<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 { 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 useTemplateName = () => {
|
||||
const { template } = useParams()
|
||||
const templatePermissions = (templateId: string) => ({
|
||||
canUpdateTemplate: {
|
||||
object: {
|
||||
resource_type: "template",
|
||||
resource_id: templateId,
|
||||
},
|
||||
action: "update",
|
||||
},
|
||||
})
|
||||
|
||||
if (!template) {
|
||||
throw new Error("No template found in the URL")
|
||||
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),
|
||||
}),
|
||||
])
|
||||
|
||||
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<ReturnType<typeof fetchTemplate>>
|
||||
|
||||
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 (
|
||||
<div className={styles.error}>
|
||||
<AlertBanner severity="error" error={getTemplateError} />
|
||||
<AlertBanner severity="error" error={templateData.error} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!template || !templatePermissions) {
|
||||
if (templateData.isLoading || !templateData.data) {
|
||||
return <Loader />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TemplatePageHeader
|
||||
template={template}
|
||||
permissions={templatePermissions}
|
||||
template={templateData.data.template}
|
||||
activeVersion={templateData.data.activeVersion}
|
||||
permissions={templateData.data.permissions}
|
||||
canEditFiles={dashboard.experiments.includes("template_editor")}
|
||||
onDeleteTemplate={() => {
|
||||
navigate("/templates")
|
||||
}}
|
||||
@@ -92,7 +104,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
|
||||
<Stack direction="row" spacing={0.25}>
|
||||
<NavLink
|
||||
end
|
||||
to={`/templates/${template.name}`}
|
||||
to={`/templates/${template}`}
|
||||
className={({ isActive }) =>
|
||||
combineClasses([
|
||||
styles.tabItem,
|
||||
@@ -103,7 +115,7 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
|
||||
Summary
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to={`/templates/${template.name}/permissions`}
|
||||
to={`/templates/${template}/permissions`}
|
||||
className={({ isActive }) =>
|
||||
combineClasses([
|
||||
styles.tabItem,
|
||||
@@ -113,14 +125,23 @@ export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
|
||||
>
|
||||
Permissions
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to={`/templates/${template}/files`}
|
||||
className={({ isActive }) =>
|
||||
combineClasses([
|
||||
styles.tabItem,
|
||||
isActive ? styles.tabItemActive : undefined,
|
||||
])
|
||||
}
|
||||
>
|
||||
Source Code
|
||||
</NavLink>
|
||||
</Stack>
|
||||
</Margins>
|
||||
</div>
|
||||
|
||||
<Margins>
|
||||
<TemplateLayoutContext.Provider
|
||||
value={{ permissions, context: templateState.context }}
|
||||
>
|
||||
<TemplateLayoutContext.Provider value={templateData.data}>
|
||||
<Suspense fallback={<Loader />}>{children}</Suspense>
|
||||
</TemplateLayoutContext.Provider>
|
||||
</Margins>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}) => (
|
||||
<Button
|
||||
variant="outlined"
|
||||
component={RouterLink}
|
||||
to={`/templates/${templateName}/settings`}
|
||||
startIcon={<SettingsOutlined />}
|
||||
>
|
||||
{Language.settingsButton}
|
||||
</Button>
|
||||
)
|
||||
const TemplateMenu: FC<{
|
||||
templateName: string
|
||||
templateVersion: string
|
||||
canEditFiles: boolean
|
||||
onDelete: () => void
|
||||
}> = ({ templateName, templateVersion, canEditFiles, onDelete }) => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null)
|
||||
|
||||
const TemplateVariablesButton: FC<{ templateName: string }> = ({
|
||||
templateName,
|
||||
}) => (
|
||||
<Button
|
||||
variant="outlined"
|
||||
component={RouterLink}
|
||||
to={`/templates/${templateName}/variables`}
|
||||
startIcon={<CodeOutlined />}
|
||||
>
|
||||
{Language.variablesButton}
|
||||
</Button>
|
||||
)
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
variant="outlined"
|
||||
aria-controls="template-options"
|
||||
aria-haspopup="true"
|
||||
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<{
|
||||
templateName: string
|
||||
@@ -65,21 +109,19 @@ const CreateWorkspaceButton: FC<{
|
||||
</Button>
|
||||
)
|
||||
|
||||
const DeleteTemplateButton: FC<{ onClick: () => void }> = ({ onClick }) => (
|
||||
<Button variant="outlined" startIcon={<DeleteOutlined />} onClick={onClick}>
|
||||
{Language.deleteButton}
|
||||
</Button>
|
||||
)
|
||||
|
||||
export type TemplatePageHeaderProps = {
|
||||
template: Template
|
||||
activeVersion: TemplateVersion
|
||||
permissions: AuthorizationResponse
|
||||
canEditFiles: boolean
|
||||
onDeleteTemplate: () => void
|
||||
}
|
||||
|
||||
export const TemplatePageHeader: FC<TemplatePageHeaderProps> = ({
|
||||
template,
|
||||
activeVersion,
|
||||
permissions,
|
||||
canEditFiles,
|
||||
onDeleteTemplate,
|
||||
}) => {
|
||||
const hasIcon = template.icon && template.icon !== ""
|
||||
@@ -90,14 +132,15 @@ export const TemplatePageHeader: FC<TemplatePageHeaderProps> = ({
|
||||
<PageHeader
|
||||
actions={
|
||||
<>
|
||||
<Maybe condition={permissions.canUpdateTemplate}>
|
||||
<DeleteTemplateButton
|
||||
onClick={deleteTemplate.openDeleteConfirmation}
|
||||
/>
|
||||
<TemplateSettingsButton templateName={template.name} />
|
||||
<TemplateVariablesButton templateName={template.name} />
|
||||
</Maybe>
|
||||
<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>
|
||||
> = () => {
|
||||
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
|
||||
|
||||
|
||||
@@ -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 <Loader />
|
||||
}
|
||||
const { template, activeVersion } = useTemplateLayoutContext()
|
||||
const { data } = useTemplateSummaryData(
|
||||
template.id,
|
||||
template.active_version_id,
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -33,11 +26,9 @@ export const TemplateSummaryPage: FC = () => {
|
||||
</title>
|
||||
</Helmet>
|
||||
<TemplateSummaryPageView
|
||||
data={data}
|
||||
template={template}
|
||||
activeTemplateVersion={activeTemplateVersion}
|
||||
templateResources={templateResources}
|
||||
templateVersions={templateVersions}
|
||||
templateDAUs={templateDAUs}
|
||||
activeVersion={activeVersion}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
+27
-27
@@ -17,47 +17,47 @@ const Template: Story<TemplateSummaryPageViewProps> = (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] },
|
||||
|
||||
@@ -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<TemplateSummaryPageViewProps>
|
||||
> = ({
|
||||
export const TemplateSummaryPageView: FC<TemplateSummaryPageViewProps> = ({
|
||||
data,
|
||||
template,
|
||||
activeTemplateVersion,
|
||||
templateResources,
|
||||
templateVersions,
|
||||
templateDAUs,
|
||||
activeVersion,
|
||||
}) => {
|
||||
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[]) => {
|
||||
return resources.filter(
|
||||
@@ -42,14 +43,9 @@ export const TemplateSummaryPageView: FC<
|
||||
|
||||
return (
|
||||
<Stack spacing={4}>
|
||||
<TemplateStats
|
||||
template={template}
|
||||
activeVersion={activeTemplateVersion}
|
||||
/>
|
||||
{templateDAUs && <DAUChart daus={templateDAUs} />}
|
||||
<TemplateResourcesTable
|
||||
resources={getStartedResources(templateResources)}
|
||||
/>
|
||||
<TemplateStats template={template} activeVersion={activeVersion} />
|
||||
{daus && <DAUChart daus={daus} />}
|
||||
<TemplateResourcesTable resources={getStartedResources(resources)} />
|
||||
|
||||
<div className={styles.markdownSection} id="readme">
|
||||
<div className={styles.readmeLabel}>README.md</div>
|
||||
@@ -58,7 +54,7 @@ export const TemplateSummaryPageView: FC<
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VersionsTable versions={templateVersions} />
|
||||
<VersionsTable versions={versions} />
|
||||
</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 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<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 {
|
||||
/**
|
||||
* Used to display the version name before loading the version in the API
|
||||
@@ -165,7 +86,7 @@ export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
|
||||
/>
|
||||
</Stats>
|
||||
|
||||
<Files
|
||||
<TemplateFiles
|
||||
tab={tab}
|
||||
currentFiles={currentFiles}
|
||||
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
|
||||
|
||||
@@ -234,5 +234,13 @@ export const getOverrides = ({
|
||||
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