mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat: Add template version page (#5071)
This commit is contained in:
+4
-1
@@ -340,7 +340,10 @@ func New(options *Options) *API {
|
||||
httpmw.ExtractOrganizationParam(options.Database),
|
||||
)
|
||||
r.Get("/", api.organization)
|
||||
r.Post("/templateversions", api.postTemplateVersionsByOrganization)
|
||||
r.Route("/templateversions", func(r chi.Router) {
|
||||
r.Post("/", api.postTemplateVersionsByOrganization)
|
||||
r.Get("/{templateversionname}", api.templateVersionByOrganizationAndName)
|
||||
})
|
||||
r.Route("/templates", func(r chi.Router) {
|
||||
r.Post("/", api.postTemplateByOrganization)
|
||||
r.Get("/", api.templatesByOrganization)
|
||||
|
||||
@@ -238,10 +238,11 @@ func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
|
||||
"GET:/api/v2/applications/auth-redirect": {AssertAction: rbac.ActionCreate, AssertObject: rbac.ResourceAPIKey},
|
||||
|
||||
// These endpoints need payloads to get to the auth part. Payloads will be required
|
||||
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
||||
"PUT:/api/v2/organizations/{organization}/members/{user}/roles": {NoAuthorize: true},
|
||||
"POST:/api/v2/workspaces/{workspace}/builds": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
||||
"POST:/api/v2/organizations/{organization}/templateversions": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
||||
"PUT:/api/v2/users/{user}/roles": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
||||
"PUT:/api/v2/organizations/{organization}/members/{user}/roles": {NoAuthorize: true},
|
||||
"POST:/api/v2/workspaces/{workspace}/builds": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
||||
"POST:/api/v2/organizations/{organization}/templateversions": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
||||
"GET:/api/v2/organizations/{organization}/templateversions/{templateversionname}": {StatusCode: http.StatusBadRequest, NoAuthorize: true},
|
||||
|
||||
// Endpoints that use the SQLQuery filter.
|
||||
"GET:/api/v2/workspaces/": {StatusCode: http.StatusOK, NoAuthorize: true},
|
||||
|
||||
@@ -1471,6 +1471,22 @@ func (q *fakeQuerier) GetTemplateVersionByTemplateIDAndName(_ context.Context, a
|
||||
return database.TemplateVersion{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetTemplateVersionByOrganizationAndName(_ context.Context, arg database.GetTemplateVersionByOrganizationAndNameParams) (database.TemplateVersion, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
for _, templateVersion := range q.templateVersions {
|
||||
if templateVersion.OrganizationID != arg.OrganizationID {
|
||||
continue
|
||||
}
|
||||
if !strings.EqualFold(templateVersion.Name, arg.Name) {
|
||||
continue
|
||||
}
|
||||
return templateVersion, nil
|
||||
}
|
||||
return database.TemplateVersion{}, sql.ErrNoRows
|
||||
}
|
||||
|
||||
func (q *fakeQuerier) GetTemplateVersionByID(_ context.Context, templateVersionID uuid.UUID) (database.TemplateVersion, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
@@ -81,6 +81,7 @@ type sqlcQuerier interface {
|
||||
GetTemplateDAUs(ctx context.Context, templateID uuid.UUID) ([]GetTemplateDAUsRow, error)
|
||||
GetTemplateVersionByID(ctx context.Context, id uuid.UUID) (TemplateVersion, error)
|
||||
GetTemplateVersionByJobID(ctx context.Context, jobID uuid.UUID) (TemplateVersion, error)
|
||||
GetTemplateVersionByOrganizationAndName(ctx context.Context, arg GetTemplateVersionByOrganizationAndNameParams) (TemplateVersion, error)
|
||||
GetTemplateVersionByTemplateIDAndName(ctx context.Context, arg GetTemplateVersionByTemplateIDAndNameParams) (TemplateVersion, error)
|
||||
GetTemplateVersionsByTemplateID(ctx context.Context, arg GetTemplateVersionsByTemplateIDParams) ([]TemplateVersion, error)
|
||||
GetTemplateVersionsCreatedAfter(ctx context.Context, createdAt time.Time) ([]TemplateVersion, error)
|
||||
|
||||
@@ -3550,6 +3550,38 @@ func (q *sqlQuerier) GetTemplateVersionByJobID(ctx context.Context, jobID uuid.U
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getTemplateVersionByOrganizationAndName = `-- name: GetTemplateVersionByOrganizationAndName :one
|
||||
SELECT
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by
|
||||
FROM
|
||||
template_versions
|
||||
WHERE
|
||||
organization_id = $1
|
||||
AND "name" = $2
|
||||
`
|
||||
|
||||
type GetTemplateVersionByOrganizationAndNameParams struct {
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetTemplateVersionByOrganizationAndName(ctx context.Context, arg GetTemplateVersionByOrganizationAndNameParams) (TemplateVersion, error) {
|
||||
row := q.db.QueryRowContext(ctx, getTemplateVersionByOrganizationAndName, arg.OrganizationID, arg.Name)
|
||||
var i TemplateVersion
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.TemplateID,
|
||||
&i.OrganizationID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.Name,
|
||||
&i.Readme,
|
||||
&i.JobID,
|
||||
&i.CreatedBy,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getTemplateVersionByTemplateIDAndName = `-- name: GetTemplateVersionByTemplateIDAndName :one
|
||||
SELECT
|
||||
id, template_id, organization_id, created_at, updated_at, name, readme, job_id, created_by
|
||||
|
||||
@@ -52,6 +52,15 @@ WHERE
|
||||
template_id = $1
|
||||
AND "name" = $2;
|
||||
|
||||
-- name: GetTemplateVersionByOrganizationAndName :one
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
template_versions
|
||||
WHERE
|
||||
organization_id = $1
|
||||
AND "name" = $2;
|
||||
|
||||
-- name: GetTemplateVersionByID :one
|
||||
SELECT
|
||||
*
|
||||
|
||||
@@ -596,6 +596,48 @@ func (api *API) templateVersionByName(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(job), user))
|
||||
}
|
||||
|
||||
func (api *API) templateVersionByOrganizationAndName(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
organization := httpmw.OrganizationParam(r)
|
||||
templateVersionName := chi.URLParam(r, "templateversionname")
|
||||
templateVersion, err := api.Database.GetTemplateVersionByOrganizationAndName(ctx, database.GetTemplateVersionByOrganizationAndNameParams{
|
||||
OrganizationID: organization.ID,
|
||||
Name: templateVersionName,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
|
||||
Message: fmt.Sprintf("No template version found by name %q.", templateVersionName),
|
||||
})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching template version.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
job, err := api.Database.GetProvisionerJobByID(ctx, templateVersion.JobID)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching provisioner job.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := api.Database.GetUserByID(ctx, templateVersion.CreatedBy)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error on fetching user.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertTemplateVersion(templateVersion, convertProvisionerJob(job), user))
|
||||
}
|
||||
|
||||
func (api *API) patchActiveTemplateVersion(rw http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
ctx = r.Context()
|
||||
|
||||
@@ -928,3 +928,36 @@ func TestPaginatedTemplateVersions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateVersionByOrganizationAndName(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.TemplateVersionByOrganizationAndName(ctx, user.OrganizationID, "nothing")
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusNotFound, apiErr.StatusCode())
|
||||
})
|
||||
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := coderdtest.New(t, nil)
|
||||
user := coderdtest.CreateFirstUser(t, client)
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
||||
coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
_, err := client.TemplateVersionByOrganizationAndName(ctx, user.OrganizationID, version.Name)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -138,6 +138,25 @@ func (c *Client) CreateTemplateVersion(ctx context.Context, organizationID uuid.
|
||||
return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion)
|
||||
}
|
||||
|
||||
func (c *Client) TemplateVersionByOrganizationAndName(ctx context.Context, organizationID uuid.UUID, name string) (TemplateVersion, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet,
|
||||
fmt.Sprintf("/api/v2/organizations/%s/templateversions/%s", organizationID.String(), name),
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return TemplateVersion{}, xerrors.Errorf("execute request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return TemplateVersion{}, readBodyAsError(res)
|
||||
}
|
||||
|
||||
var templateVersion TemplateVersion
|
||||
return templateVersion, json.NewDecoder(res.Body).Decode(&templateVersion)
|
||||
}
|
||||
|
||||
// CreateTemplate creates a new template inside an organization.
|
||||
func (c *Client) CreateTemplate(ctx context.Context, organizationID uuid.UUID, request CreateTemplateRequest) (Template, error) {
|
||||
res, err := c.Request(ctx, http.MethodPost,
|
||||
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
declare module "js-untar" {
|
||||
interface File {
|
||||
name: string
|
||||
readAsString: () => string
|
||||
}
|
||||
|
||||
const Untar: (buffer: ArrayBuffer) => {
|
||||
then: (
|
||||
resolve?: () => Promise<void>,
|
||||
reject?: () => Promise<void>,
|
||||
progress: (file: File) => Promise<void>,
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
export default Untar
|
||||
}
|
||||
@@ -50,6 +50,7 @@
|
||||
"front-matter": "4.0.2",
|
||||
"history": "5.3.0",
|
||||
"i18next": "21.9.1",
|
||||
"js-untar": "2.0.0",
|
||||
"just-debounce-it": "3.1.1",
|
||||
"react": "18.2.0",
|
||||
"react-chartjs-2": "4.3.1",
|
||||
|
||||
+21
-10
@@ -83,6 +83,9 @@ const NetworkSettingsPage = lazy(
|
||||
() => import("./pages/DeploySettingsPage/NetworkSettingsPage"),
|
||||
)
|
||||
const GitAuthPage = lazy(() => import("./pages/GitAuthPage/GitAuthPage"))
|
||||
const TemplateVersionPage = lazy(
|
||||
() => import("./pages/TemplateVersionPage/TemplateVersionPage"),
|
||||
)
|
||||
|
||||
export const AppRouter: FC = () => {
|
||||
const xServices = useContext(XServiceContext)
|
||||
@@ -123,16 +126,14 @@ export const AppRouter: FC = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="workspaces">
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<WorkspacesPage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="workspaces"
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<WorkspacesPage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="templates">
|
||||
<Route
|
||||
@@ -181,6 +182,16 @@ export const AppRouter: FC = () => {
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route path="versions">
|
||||
<Route
|
||||
path=":version"
|
||||
element={
|
||||
<AuthAndFrame>
|
||||
<TemplateVersionPage />
|
||||
</AuthAndFrame>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export default jest.fn()
|
||||
@@ -219,6 +219,16 @@ export const getTemplateVersions = async (
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const getTemplateVersionByName = async (
|
||||
organizationId: string,
|
||||
versionName: string,
|
||||
): Promise<TypesGen.TemplateVersion> => {
|
||||
const response = await axios.get<TypesGen.TemplateVersion>(
|
||||
`/api/v2/organizations/${organizationId}/templateversions/${versionName}`,
|
||||
)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const updateTemplateMeta = async (
|
||||
templateId: string,
|
||||
data: TypesGen.UpdateTemplateMeta,
|
||||
@@ -646,3 +656,10 @@ export const getReplicas = async (): Promise<TypesGen.Replica[]> => {
|
||||
const response = await axios.get(`/api/v2/replicas`)
|
||||
return response.data
|
||||
}
|
||||
|
||||
export const getFile = async (fileId: string): Promise<ArrayBuffer> => {
|
||||
const response = await axios.get<ArrayBuffer>(`/api/v2/files/${fileId}`, {
|
||||
responseType: "arraybuffer",
|
||||
})
|
||||
return response.data
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
|
||||
|
||||
export const MarkdownIcon = (props: SvgIconProps): JSX.Element => (
|
||||
<SvgIcon {...props} viewBox="0 0 32 32">
|
||||
<rect
|
||||
x="2.5"
|
||||
y="7.955"
|
||||
width="27"
|
||||
height="16.091"
|
||||
style={{ fill: "none", stroke: "#755838" }}
|
||||
/>
|
||||
<polygon
|
||||
points="5.909 20.636 5.909 11.364 8.636 11.364 11.364 14.773 14.091 11.364 16.818 11.364 16.818 20.636 14.091 20.636 14.091 15.318 11.364 18.727 8.636 15.318 8.636 20.636 5.909 20.636"
|
||||
style={{ stroke: "#755838" }}
|
||||
/>
|
||||
<polygon
|
||||
points="22.955 20.636 18.864 16.136 21.591 16.136 21.591 11.364 24.318 11.364 24.318 16.136 27.045 16.136 22.955 20.636"
|
||||
style={{ stroke: "#755838" }}
|
||||
/>
|
||||
</SvgIcon>
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"
|
||||
|
||||
export const TerraformIcon = (props: SvgIconProps): JSX.Element => (
|
||||
<SvgIcon {...props} viewBox="0 0 32 32">
|
||||
<polygon
|
||||
points="12.042 6.858 20.071 11.448 20.071 20.462 12.042 15.868 12.042 6.858 12.042 6.858"
|
||||
style={{ fill: "#813cf3" }}
|
||||
/>
|
||||
<polygon
|
||||
points="20.5 20.415 28.459 15.84 28.459 6.887 20.5 11.429 20.5 20.415 20.5 20.415"
|
||||
style={{ fill: "#813cf3" }}
|
||||
/>
|
||||
<polygon
|
||||
points="3.541 11.01 11.571 15.599 11.571 6.59 3.541 2 3.541 11.01 3.541 11.01"
|
||||
style={{ fill: "#813cf3" }}
|
||||
/>
|
||||
<polygon
|
||||
points="12.042 25.41 20.071 30 20.071 20.957 12.042 16.368 12.042 25.41 12.042 25.41"
|
||||
style={{ fill: "#813cf3" }}
|
||||
/>
|
||||
</SvgIcon>
|
||||
)
|
||||
@@ -1,4 +1,5 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { PropsWithChildren, FC } from "react"
|
||||
import { combineClasses } from "../../util/combineClasses"
|
||||
import { Stack } from "../Stack/Stack"
|
||||
|
||||
@@ -7,7 +8,7 @@ export interface PageHeaderProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const PageHeader: React.FC<React.PropsWithChildren<PageHeaderProps>> = ({
|
||||
export const PageHeader: FC<PropsWithChildren<PageHeaderProps>> = ({
|
||||
children,
|
||||
actions,
|
||||
className,
|
||||
@@ -29,7 +30,7 @@ export const PageHeader: React.FC<React.PropsWithChildren<PageHeaderProps>> = ({
|
||||
)
|
||||
}
|
||||
|
||||
export const PageHeaderTitle: React.FC<React.PropsWithChildren<unknown>> = ({
|
||||
export const PageHeaderTitle: FC<PropsWithChildren<unknown>> = ({
|
||||
children,
|
||||
}) => {
|
||||
const styles = useStyles({})
|
||||
@@ -37,8 +38,8 @@ export const PageHeaderTitle: React.FC<React.PropsWithChildren<unknown>> = ({
|
||||
return <h1 className={styles.title}>{children}</h1>
|
||||
}
|
||||
|
||||
export const PageHeaderSubtitle: React.FC<
|
||||
React.PropsWithChildren<{ condensed?: boolean }>
|
||||
export const PageHeaderSubtitle: FC<
|
||||
PropsWithChildren<{ condensed?: boolean }>
|
||||
> = ({ children, condensed }) => {
|
||||
const styles = useStyles({
|
||||
condensed,
|
||||
@@ -47,6 +48,11 @@ export const PageHeaderSubtitle: React.FC<
|
||||
return <h2 className={styles.subtitle}>{children}</h2>
|
||||
}
|
||||
|
||||
export const PageHeaderCaption: FC<PropsWithChildren> = ({ children }) => {
|
||||
const styles = useStyles({})
|
||||
return <span className={styles.caption}>{children}</span>
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
display: "flex",
|
||||
@@ -88,4 +94,12 @@ const useStyles = makeStyles((theme) => ({
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
|
||||
caption: {
|
||||
fontSize: 12,
|
||||
color: theme.palette.text.secondary,
|
||||
fontWeight: 600,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.1em",
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { ComponentProps, FC } from "react"
|
||||
import { Prism } from "react-syntax-highlighter"
|
||||
import { colors } from "theme/colors"
|
||||
import darcula from "react-syntax-highlighter/dist/cjs/styles/prism/darcula"
|
||||
import { combineClasses } from "util/combineClasses"
|
||||
|
||||
export const SyntaxHighlighter: FC<ComponentProps<typeof Prism>> = ({
|
||||
className,
|
||||
...props
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<Prism
|
||||
style={darcula}
|
||||
useInlineStyles={false}
|
||||
// Use inline styles does not work correctly
|
||||
// https://github.com/react-syntax-highlighter/react-syntax-highlighter/issues/329
|
||||
codeTagProps={{ style: {} }}
|
||||
className={combineClasses([styles.prism, className])}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
prism: {
|
||||
margin: 0,
|
||||
background: theme.palette.background.paperLight,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
padding: theme.spacing(2, 3),
|
||||
|
||||
"& code": {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
|
||||
"& .key, & .property, & .code-snippet, & .keyword": {
|
||||
color: colors.turquoise[7],
|
||||
},
|
||||
|
||||
"& .url": {
|
||||
color: colors.blue[6],
|
||||
},
|
||||
|
||||
"& .comment": {
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
|
||||
"& .title": {
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
}))
|
||||
@@ -1,5 +1,6 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { Stats, StatsItem } from "components/Stats/Stats"
|
||||
import { FC } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import { createDayString } from "util/createDayString"
|
||||
import {
|
||||
formatTemplateBuildTime,
|
||||
@@ -26,77 +27,39 @@ export const TemplateStats: FC<TemplateStatsProps> = ({
|
||||
template,
|
||||
activeVersion,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<div className={styles.stats}>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statsLabel}>{Language.usedByLabel}:</span>
|
||||
|
||||
<span className={styles.statsValue}>
|
||||
{formatTemplateActiveDevelopers(template.active_user_count)}{" "}
|
||||
{template.active_user_count === 1
|
||||
? Language.developerSingular
|
||||
: Language.developerPlural}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statsLabel}>{Language.buildTimeLabel}:</span>
|
||||
|
||||
<span className={styles.statsValue}>
|
||||
{formatTemplateBuildTime(template.build_time_stats.start_ms)}{" "}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statsLabel}>
|
||||
{Language.activeVersionLabel}:
|
||||
</span>
|
||||
<span className={styles.statsValue}>{activeVersion.name}</span>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statsLabel}>{Language.lastUpdateLabel}:</span>
|
||||
<span className={styles.statsValue} data-chromatic="ignore">
|
||||
{createDayString(template.updated_at)}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.statItem}>
|
||||
<span className={styles.statsLabel}>{Language.createdByLabel}:</span>
|
||||
<span className={styles.statsValue}>{template.created_by_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Stats>
|
||||
<StatsItem
|
||||
label={Language.usedByLabel}
|
||||
value={
|
||||
<>
|
||||
{formatTemplateActiveDevelopers(template.active_user_count)}{" "}
|
||||
{template.active_user_count === 1
|
||||
? Language.developerSingular
|
||||
: Language.developerPlural}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<StatsItem
|
||||
label={Language.buildTimeLabel}
|
||||
value={formatTemplateBuildTime(template.build_time_stats.start_ms)}
|
||||
/>
|
||||
<StatsItem
|
||||
label={Language.activeVersionLabel}
|
||||
value={
|
||||
<Link to={`versions/${activeVersion.name}`}>
|
||||
{activeVersion.name}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<StatsItem
|
||||
label={Language.lastUpdateLabel}
|
||||
value={createDayString(template.updated_at)}
|
||||
/>
|
||||
<StatsItem
|
||||
label={Language.createdByLabel}
|
||||
value={template.created_by_name}
|
||||
/>
|
||||
</Stats>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
stats: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
color: theme.palette.text.secondary,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
[theme.breakpoints.down("sm")]: {
|
||||
display: "block",
|
||||
},
|
||||
},
|
||||
|
||||
statItem: {
|
||||
padding: theme.spacing(2),
|
||||
paddingTop: theme.spacing(1.75),
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
|
||||
statsLabel: {
|
||||
display: "block",
|
||||
wordWrap: "break-word",
|
||||
},
|
||||
|
||||
statsValue: {
|
||||
display: "block",
|
||||
wordWrap: "break-word",
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -4,7 +4,9 @@ import TableRow from "@material-ui/core/TableRow"
|
||||
import { TemplateVersion } from "api/typesGenerated"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { UserAvatar } from "components/UserAvatar/UserAvatar"
|
||||
import { useClickable } from "hooks/useClickable"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
|
||||
export interface VersionRowProps {
|
||||
version: TemplateVersion
|
||||
@@ -13,11 +15,16 @@ export interface VersionRowProps {
|
||||
export const VersionRow: React.FC<VersionRowProps> = ({ version }) => {
|
||||
const styles = useStyles()
|
||||
const { t } = useTranslation("templatePage")
|
||||
const navigate = useNavigate()
|
||||
const clickableProps = useClickable(() => {
|
||||
navigate(`versions/${version.name}`)
|
||||
})
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
className={styles.versionRow}
|
||||
data-testid={`version-${version.id}`}
|
||||
{...clickableProps}
|
||||
>
|
||||
<TableCell className={styles.versionCell}>
|
||||
<Stack
|
||||
@@ -54,6 +61,12 @@ export const VersionRow: React.FC<VersionRowProps> = ({ version }) => {
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
versionRow: {
|
||||
cursor: "pointer",
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
|
||||
"&:not(:last-child) td:before": {
|
||||
position: "absolute",
|
||||
top: 20,
|
||||
|
||||
@@ -132,6 +132,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: 600,
|
||||
},
|
||||
|
||||
outdatedLabel: {
|
||||
color: theme.palette.error.main,
|
||||
display: "flex",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useSearchParams } from "react-router-dom"
|
||||
|
||||
export interface UseTabResult {
|
||||
value: string | null
|
||||
set: (value: string) => void
|
||||
}
|
||||
|
||||
export const useTab = (tabKey: string): UseTabResult => {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const value = searchParams.get(tabKey)
|
||||
|
||||
return {
|
||||
value,
|
||||
set: (value: string) => {
|
||||
searchParams.set(tabKey, value)
|
||||
setSearchParams(searchParams)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import agent from "./agent.json"
|
||||
import buildPage from "./buildPage.json"
|
||||
import workspacesPage from "./workspacesPage.json"
|
||||
import usersPage from "./usersPage.json"
|
||||
import templateVersionPage from "./templateVersionPage.json"
|
||||
|
||||
export const en = {
|
||||
common,
|
||||
@@ -20,4 +21,5 @@ export const en = {
|
||||
buildPage,
|
||||
workspacesPage,
|
||||
usersPage,
|
||||
templateVersionPage,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"title": "Version",
|
||||
"header": {
|
||||
"caption": "Version"
|
||||
},
|
||||
"stats": {
|
||||
"template": "Template",
|
||||
"createdBy": "Created by",
|
||||
"created": "Created"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
renderWithAuth,
|
||||
waitForLoaderToBeRemoved,
|
||||
} from "testHelpers/renderHelpers"
|
||||
import TemplateVersionPage from "./TemplateVersionPage"
|
||||
import * as templateVersionUtils from "util/templateVersion"
|
||||
import { screen } from "@testing-library/react"
|
||||
import * as CreateDayString from "util/createDayString"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
|
||||
const TEMPLATE_NAME = "coder-ts"
|
||||
const VERSION_NAME = "12345"
|
||||
const TERRAFORM_FILENAME = "main.tf"
|
||||
const README_FILENAME = "readme.md"
|
||||
const GPG_FILENAME = "key.gpg"
|
||||
const TEMPLATE_VERSION_FILES = {
|
||||
[TERRAFORM_FILENAME]: "{}",
|
||||
[README_FILENAME]: "Readme",
|
||||
[GPG_FILENAME]: "Some sensitive info",
|
||||
}
|
||||
|
||||
const setup = async () => {
|
||||
jest
|
||||
.spyOn(templateVersionUtils, "getTemplateVersionFiles")
|
||||
.mockResolvedValueOnce(TEMPLATE_VERSION_FILES)
|
||||
|
||||
jest
|
||||
.spyOn(CreateDayString, "createDayString")
|
||||
.mockImplementation(() => "a minute ago")
|
||||
|
||||
renderWithAuth(<TemplateVersionPage />, {
|
||||
route: `/templates/${TEMPLATE_NAME}/versions/${VERSION_NAME}`,
|
||||
path: "/templates/:template/versions/:version",
|
||||
})
|
||||
await waitForLoaderToBeRemoved()
|
||||
}
|
||||
|
||||
describe("TemplateVersionPage", () => {
|
||||
beforeEach(setup)
|
||||
|
||||
it("shows the tf and md files only", () => {
|
||||
expect(screen.queryByText(TERRAFORM_FILENAME)).toBeInTheDocument()
|
||||
expect(screen.queryByText(README_FILENAME)).toBeInTheDocument()
|
||||
expect(screen.queryByText(GPG_FILENAME)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("shows the right content when click on the file name", async () => {
|
||||
await userEvent.click(screen.getByText(README_FILENAME))
|
||||
expect(
|
||||
screen.queryByText(TEMPLATE_VERSION_FILES[README_FILENAME]),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { useOrganizationId } from "hooks/useOrganizationId"
|
||||
import { useTab } from "hooks/useTab"
|
||||
import { FC } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { pageTitle } from "util/page"
|
||||
import { templateVersionMachine } from "xServices/templateVersion/templateVersionXService"
|
||||
import TemplateVersionPageView from "./TemplateVersionPageView"
|
||||
|
||||
type Params = {
|
||||
version: string
|
||||
template: string
|
||||
}
|
||||
|
||||
export const TemplateVersionPage: FC = () => {
|
||||
const { version: versionName, template: templateName } = useParams() as Params
|
||||
const orgId = useOrganizationId()
|
||||
const [state] = useMachine(templateVersionMachine, {
|
||||
context: { versionName, orgId },
|
||||
})
|
||||
const tab = useTab("file")
|
||||
const { t } = useTranslation("templateVersionPage")
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>
|
||||
{pageTitle(`${t("title")} ${versionName} · ${templateName}`)}
|
||||
</title>
|
||||
</Helmet>
|
||||
|
||||
<TemplateVersionPageView
|
||||
context={state.context}
|
||||
versionName={versionName}
|
||||
templateName={templateName}
|
||||
tab={tab}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TemplateVersionPage
|
||||
@@ -0,0 +1,69 @@
|
||||
import { action } from "@storybook/addon-actions"
|
||||
import { Story } from "@storybook/react"
|
||||
import { UseTabResult } from "hooks/useTab"
|
||||
import {
|
||||
makeMockApiError,
|
||||
MockOrganization,
|
||||
MockTemplate,
|
||||
MockTemplateVersion,
|
||||
} from "testHelpers/entities"
|
||||
import {
|
||||
TemplateVersionPageView,
|
||||
TemplateVersionPageViewProps,
|
||||
} from "./TemplateVersionPageView"
|
||||
|
||||
export default {
|
||||
title: "pages/TemplateVersionPageView",
|
||||
component: TemplateVersionPageView,
|
||||
}
|
||||
|
||||
const Template: Story<TemplateVersionPageViewProps> = (args) => (
|
||||
<TemplateVersionPageView {...args} />
|
||||
)
|
||||
|
||||
const tab: UseTabResult = {
|
||||
value: "0",
|
||||
set: action("changeTab"),
|
||||
}
|
||||
|
||||
const readmeContent = `---
|
||||
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.
|
||||
\`\`\``
|
||||
|
||||
const defaultArgs = {
|
||||
tab,
|
||||
templateName: MockTemplate.name,
|
||||
versionName: MockTemplateVersion.name,
|
||||
context: {
|
||||
orgId: MockOrganization.id,
|
||||
versionName: MockTemplateVersion.name,
|
||||
version: MockTemplateVersion,
|
||||
files: {
|
||||
"README.md": readmeContent,
|
||||
"main.tf": `{}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Default = Template.bind({})
|
||||
Default.args = defaultArgs
|
||||
|
||||
export const Error = Template.bind({})
|
||||
Error.args = {
|
||||
...defaultArgs,
|
||||
context: {
|
||||
...defaultArgs.context,
|
||||
version: undefined,
|
||||
files: undefined,
|
||||
error: makeMockApiError({
|
||||
message: "Error on loading the template version",
|
||||
}),
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { AlertBanner } from "components/AlertBanner/AlertBanner"
|
||||
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 {
|
||||
PageHeader,
|
||||
PageHeaderCaption,
|
||||
PageHeaderTitle,
|
||||
} from "components/PageHeader/PageHeader"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { Stats, StatsItem } from "components/Stats/Stats"
|
||||
import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter"
|
||||
import { UseTabResult } from "hooks/useTab"
|
||||
import { FC } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { combineClasses } from "util/combineClasses"
|
||||
import { createDayString } from "util/createDayString"
|
||||
import { TemplateVersionMachineContext } from "xServices/templateVersion/templateVersionXService"
|
||||
|
||||
export interface TemplateVersionPageViewProps {
|
||||
/**
|
||||
* Used to display the version name before loading the version in the API
|
||||
*/
|
||||
versionName: string
|
||||
templateName: string
|
||||
tab: UseTabResult
|
||||
context: TemplateVersionMachineContext
|
||||
}
|
||||
|
||||
export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
|
||||
context,
|
||||
tab,
|
||||
versionName,
|
||||
templateName,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const { files, error, version } = context
|
||||
const { t } = useTranslation("templateVersionPage")
|
||||
|
||||
return (
|
||||
<Margins>
|
||||
<PageHeader>
|
||||
<PageHeaderCaption>{t("header.caption")}</PageHeaderCaption>
|
||||
<PageHeaderTitle>{versionName}</PageHeaderTitle>
|
||||
</PageHeader>
|
||||
|
||||
{!files && !error && <Loader />}
|
||||
|
||||
<Stack spacing={4}>
|
||||
{Boolean(error) && <AlertBanner severity="error" error={error} />}
|
||||
{version && files && (
|
||||
<>
|
||||
<Stats>
|
||||
<StatsItem
|
||||
label={t("stats.template")}
|
||||
value={
|
||||
<Link to={`/templates/${templateName}`}>{templateName}</Link>
|
||||
}
|
||||
/>
|
||||
<StatsItem
|
||||
label={t("stats.createdBy")}
|
||||
value={version.created_by.username}
|
||||
/>
|
||||
<StatsItem
|
||||
label={t("stats.created")}
|
||||
value={createDayString(version.created_at)}
|
||||
/>
|
||||
</Stats>
|
||||
|
||||
<div className={styles.files}>
|
||||
<div className={styles.tabs}>
|
||||
{Object.keys(files).map((filename, index) => {
|
||||
const tabValue = index.toString()
|
||||
|
||||
return (
|
||||
<button
|
||||
className={combineClasses({
|
||||
[styles.tab]: true,
|
||||
[styles.tabActive]: tabValue === tab.value,
|
||||
})}
|
||||
onClick={() => {
|
||||
tab.set(tabValue)
|
||||
}}
|
||||
key={filename}
|
||||
>
|
||||
{filename.endsWith("tf") ? (
|
||||
<TerraformIcon />
|
||||
) : (
|
||||
<MarkdownIcon />
|
||||
)}
|
||||
{filename}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<SyntaxHighlighter
|
||||
showLineNumbers
|
||||
className={styles.prism}
|
||||
language={
|
||||
Object.keys(files)[Number(tab.value)].endsWith("tf")
|
||||
? "hcl"
|
||||
: "markdown"
|
||||
}
|
||||
>
|
||||
{Object.values(files)[Number(tab.value)]}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Margins>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
tabsWrapper: {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
|
||||
tabs: {
|
||||
display: "flex",
|
||||
alignItems: "baseline",
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
|
||||
tab: {
|
||||
background: "transparent",
|
||||
border: 0,
|
||||
padding: theme.spacing(0, 3),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
height: theme.spacing(6),
|
||||
opacity: 0.75,
|
||||
cursor: "pointer",
|
||||
gap: theme.spacing(0.5),
|
||||
position: "relative",
|
||||
|
||||
"& svg": {
|
||||
width: 22,
|
||||
maxHeight: 16,
|
||||
},
|
||||
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.action.hover,
|
||||
},
|
||||
},
|
||||
|
||||
tabActive: {
|
||||
opacity: 1,
|
||||
fontWeight: 600,
|
||||
|
||||
"&:after": {
|
||||
content: '""',
|
||||
display: "block",
|
||||
height: 1,
|
||||
width: "100%",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
backgroundColor: theme.palette.secondary.dark,
|
||||
position: "absolute",
|
||||
},
|
||||
},
|
||||
|
||||
codeWrapper: {
|
||||
background: theme.palette.background.paperLight,
|
||||
},
|
||||
|
||||
files: {
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
},
|
||||
|
||||
prism: {
|
||||
borderRadius: 0,
|
||||
},
|
||||
}))
|
||||
|
||||
export default TemplateVersionPageView
|
||||
@@ -63,6 +63,12 @@ export const handlers = [
|
||||
)
|
||||
},
|
||||
),
|
||||
rest.get(
|
||||
"api/v2/organizations/:organizationId/templateversions/:templateVersionName",
|
||||
async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockTemplateVersion))
|
||||
},
|
||||
),
|
||||
rest.delete("/api/v2/templates/:templateId", async (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockTemplate))
|
||||
}),
|
||||
|
||||
@@ -37,6 +37,7 @@ export const darkPalette: PaletteOptions = {
|
||||
text: {
|
||||
primary: colors.gray[1],
|
||||
secondary: colors.gray[5],
|
||||
disabled: colors.gray[7],
|
||||
},
|
||||
divider: colors.gray[13],
|
||||
warning: {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { getFile } from "api/api"
|
||||
import { TemplateVersion } from "api/typesGenerated"
|
||||
import untar from "js-untar"
|
||||
|
||||
/**
|
||||
* Content by filename
|
||||
*/
|
||||
export type TemplateVersionFiles = Record<string, string>
|
||||
|
||||
export const getTemplateVersionFiles = async (
|
||||
version: TemplateVersion,
|
||||
): Promise<TemplateVersionFiles> => {
|
||||
const files: TemplateVersionFiles = {}
|
||||
const tarFile = await getFile(version.job.file_id)
|
||||
await untar(tarFile).then(undefined, undefined, async (file) => {
|
||||
const paths = file.name.split("/")
|
||||
const filename = paths[paths.length - 1]
|
||||
files[filename] = file.readAsString()
|
||||
})
|
||||
return files
|
||||
}
|
||||
|
||||
export const filterTemplateFilesByExtension = (
|
||||
files: TemplateVersionFiles,
|
||||
extensions: string[],
|
||||
): TemplateVersionFiles => {
|
||||
return Object.keys(files).reduce((filteredFiles, filename) => {
|
||||
const [_, extension] = filename.split(".")
|
||||
|
||||
return extensions.includes(extension)
|
||||
? { ...filteredFiles, [filename]: files[filename] }
|
||||
: filteredFiles
|
||||
}, {} as TemplateVersionFiles)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { getTemplateVersionByName } from "api/api"
|
||||
import { TemplateVersion } from "api/typesGenerated"
|
||||
import {
|
||||
filterTemplateFilesByExtension,
|
||||
getTemplateVersionFiles,
|
||||
TemplateVersionFiles,
|
||||
} from "util/templateVersion"
|
||||
import { assign, createMachine } from "xstate"
|
||||
|
||||
export interface TemplateVersionMachineContext {
|
||||
orgId: string
|
||||
versionName: string
|
||||
version?: TemplateVersion
|
||||
files?: TemplateVersionFiles
|
||||
error?: Error | unknown
|
||||
}
|
||||
|
||||
export const templateVersionMachine = createMachine(
|
||||
{
|
||||
predictableActionArguments: true,
|
||||
id: "templateVersion",
|
||||
schema: {
|
||||
context: {} as TemplateVersionMachineContext,
|
||||
services: {} as {
|
||||
loadVersion: {
|
||||
data: TemplateVersion
|
||||
}
|
||||
loadFiles: {
|
||||
data: TemplateVersionFiles
|
||||
}
|
||||
},
|
||||
},
|
||||
tsTypes: {} as import("./templateVersionXService.typegen").Typegen0,
|
||||
initial: "loadingVersion",
|
||||
states: {
|
||||
loadingVersion: {
|
||||
invoke: {
|
||||
src: "loadVersion",
|
||||
onDone: {
|
||||
target: "loadingFiles",
|
||||
actions: ["assignVersion"],
|
||||
},
|
||||
onError: {
|
||||
target: "done.error",
|
||||
actions: ["assignError"],
|
||||
},
|
||||
},
|
||||
},
|
||||
loadingFiles: {
|
||||
invoke: {
|
||||
src: "loadFiles",
|
||||
onDone: {
|
||||
target: "done.ok",
|
||||
actions: ["assignFiles"],
|
||||
},
|
||||
onError: {
|
||||
target: "done.error",
|
||||
actions: ["assignError"],
|
||||
},
|
||||
},
|
||||
},
|
||||
done: {
|
||||
states: {
|
||||
ok: { type: "final" },
|
||||
error: { type: "final" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
assignError: assign({
|
||||
error: (_, { data }) => data,
|
||||
}),
|
||||
assignVersion: assign({
|
||||
version: (_, { data }) => data,
|
||||
}),
|
||||
assignFiles: assign({
|
||||
files: (_, { data }) => data,
|
||||
}),
|
||||
},
|
||||
services: {
|
||||
loadVersion: ({ orgId, versionName }) =>
|
||||
getTemplateVersionByName(orgId, versionName),
|
||||
loadFiles: async ({ version }) => {
|
||||
if (!version) {
|
||||
throw new Error("Version is not defined")
|
||||
}
|
||||
return filterTemplateFilesByExtension(
|
||||
await getTemplateVersionFiles(version),
|
||||
["tf", "md"],
|
||||
)
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
+843
-733
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
js-untar@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-untar/-/js-untar-2.0.0.tgz#b452d28dedd3b0be92c2ac9a7d70f612a93c7453"
|
||||
integrity sha512-7CsDLrYQMbLxDt2zl9uKaPZSdmJMvGGQ7wo9hoB3J+z/VcO2w63bXFgHVnjF1+S9wD3zAu8FBVj7EYWjTQ3Z7g==
|
||||
Reference in New Issue
Block a user