UNPKG

@azure-utils/storybooks

Version:

Utils to upload and manage Storybooks via Azure Functions and storage.

500 lines (496 loc) 16.7 kB
import { DocumentLayout, ErrorMessage, __toESM, checkIsEditMode, checkIsHTMLRequest, checkIsHXRequest, checkIsNewMode, getStore, require_jsx_runtime, responseError, responseHTML, responseRedirect, urlBuilder } from "./store-BL4RNiEp.mjs"; import { CONTENT_TYPES, DEFAULT_PURGE_AFTER_DAYS, PATTERNS, commonErrorResponses } from "./constants-CsV1N9r4.mjs"; import { ProjectIdSchema } from "./shared-BAE3ceND.mjs"; import { ProjectCreateSchema, ProjectModel, ProjectSchema } from "./projects-DDVy3_77.mjs"; import { openAPITags, registerOpenAPIPath } from "./openapi-utils-CAJ85ahl.mjs"; import { joinUrl, urlSearchParamsToObject } from "./url-utils-B9Pl4bQ7.mjs"; import { ProjectsTable } from "./projects-table-B_qW44SW.mjs"; import { BuildTable, RawDataPreview } from "./builds-table-D78yU-UW.mjs"; import { LabelsTable } from "./labels-table-DoOmA3fT.mjs"; import { app } from "@azure/functions"; import z from "zod"; //#region src/components/project-form.tsx var import_jsx_runtime$1 = __toESM(require_jsx_runtime()); async function ProjectForm({ project }) { return /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("form", { "hx-ext": "response-targets", "hx-patch": project ? urlBuilder.projectId(project.id) : void 0, "hx-post": project ? void 0 : urlBuilder.allProjects(), "hx-target-error": "#form-error", style: { maxWidth: "60ch" }, children: [ /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("fieldset", { children: [ /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("legend", { children: "Details" }), project?.id ? null : /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { class: "field", children: [ /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("label", { for: "id", children: "Project ID" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("input", { id: "id", name: "id", pattern: PATTERNS.projectId.pattern, required: true }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("span", { class: "description", children: "Only lowercase alphabets, numbers and hyphen (-) allowed. Max length: 60 chars" }) ] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { class: "field", children: [/* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("label", { for: "name", children: "Project Name" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("input", { id: "name", name: "name", required: true, value: project?.name })] }) ] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("fieldset", { children: [ /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("legend", { children: "GitHub" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { class: "field", children: [/* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("label", { for: "gitHubRepo", children: "Repo name" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("input", { id: "gitHubRepo", name: "gitHubRepo", placeholder: "owner/repo", required: true, pattern: "^.+\\/.+$", value: project?.gitHubRepo })] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { class: "field", children: [ /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("label", { for: "gitHubPath", children: "Instance path" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("input", { id: "gitHubPath", name: "gitHubPath", placeholder: "packages/ui", value: project?.gitHubPath }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("span", { class: "description", children: "Optional. If the Storybook is not is the root of repo." }) ] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { class: "field", children: [ /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("label", { for: "gitHubDefaultBranch", children: "Default branch" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("input", { id: "gitHubDefaultBranch", name: "gitHubDefaultBranch", placeholder: "main", value: project?.gitHubDefaultBranch }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("span", { class: "description", children: "Optional. If the default branch is not 'main'." }) ] }) ] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("fieldset", { children: [/* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("legend", { children: "Purge" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { class: "field", children: [ /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("label", { for: "purgeBuildsAfterDays", children: "Purge builds after days" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("input", { id: "purgeBuildsAfterDays", name: "purgeBuildsAfterDays", required: true, type: "number", inputmode: "numeric", value: (project?.purgeBuildsAfterDays || DEFAULT_PURGE_AFTER_DAYS).toString() }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("span", { class: "description", safe: true, children: ProjectSchema.def.shape.purgeBuildsAfterDays.description || "" }) ] })] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { style: { display: "flex", gap: "1rem" }, children: [/* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("button", { type: "submit", children: [project ? "Update" : "Create", " Project"] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("button", { type: "reset", children: "Reset" })] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)(ErrorMessage, { id: "form-error" }) ] }); } //#endregion //#region src/handlers/project-handlers.tsx var import_jsx_runtime = __toESM(require_jsx_runtime()); async function listProjects(_request, context) { try { if (checkIsNewMode()) return responseHTML(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DocumentLayout, { title: "Create Project", breadcrumbs: [{ label: "Projects", href: urlBuilder.allProjects() }], children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProjectForm, { project: void 0 }) })); const { connectionString } = getStore(); const projectModel = new ProjectModel(context, connectionString); const projects = await projectModel.list(); if (checkIsHTMLRequest()) return responseHTML(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DocumentLayout, { title: "All Projects", toolbar: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: urlBuilder.projectCreate(), children: "+ Create" }), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProjectsTable, { projects }) })); return { status: 200, jsonBody: projects }; } catch (error) { return responseError(error, context, 500); } } async function getProject(request, context) { const { projectId } = request.params; const { connectionString } = getStore(); if (!projectId) return { status: 400, body: "Missing project ID" }; try { const projectModel = new ProjectModel(context, connectionString); const project = await projectModel.get(projectId); if (checkIsEditMode()) return responseHTML(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DocumentLayout, { title: "Edit Project", breadcrumbs: [{ label: "Projects", href: urlBuilder.allProjects() }, { label: projectId, href: urlBuilder.projectId(projectId) }], children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProjectForm, { project }) })); const builds = await projectModel.buildModel(projectId).list({ limit: 25 }); const labels = await projectModel.labelModel(projectId).list({ limit: 25 }); if (checkIsHTMLRequest()) return responseHTML(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DocumentLayout, { title: project.name, breadcrumbs: [{ label: "Projects", href: urlBuilder.allProjects() }], toolbar: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: "1rem", alignItems: "center" }, children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: urlBuilder.projectIdEdit(projectId), children: "Edit" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { "hx-delete": request.url, "hx-confirm": "Are you sure about deleting the project?", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { children: "Delete" }) })] }), children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RawDataPreview, { data: project, summary: "Project details" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LabelsTable, { caption: "Latest labels", toolbar: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: urlBuilder.allLabels(projectId), children: "View all" }), labels, projectId }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuildTable, { toolbar: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: urlBuilder.allBuilds(projectId), children: "View all" }), caption: "Latest builds", project, builds, labels }) ] }) })); return { status: 200, jsonBody: { ...project, latestBuilds: builds, labels } }; } catch (error) { return responseError(error, context, 404); } } async function createProject(request, context) { try { const { connectionString } = getStore(); const contentType = request.headers.get("content-type"); if (!contentType) return responseError("Content-Type header is required", context, 400); if (!contentType.includes(CONTENT_TYPES.FORM_ENCODED)) return responseError(`Invalid Content-Type, expected ${CONTENT_TYPES.FORM_ENCODED}`, context, 415); const data = ProjectCreateSchema.parse(urlSearchParamsToObject(await request.formData())); const projectModel = new ProjectModel(context, connectionString); await projectModel.create(data); const projectUrl = urlBuilder.projectId(data.id); if (checkIsHTMLRequest() || checkIsHXRequest()) return responseRedirect(projectUrl, 303); return { status: 201, headers: { Location: projectUrl }, jsonBody: { data, links: { self: projectUrl } } }; } catch (error) { return responseError(error, context); } } async function updateProject(request, context) { try { const { projectId } = request.params; const { connectionString } = getStore(); if (!projectId) return { status: 400, body: "Missing project ID" }; const contentType = request.headers.get("content-type"); if (!contentType) return responseError("Content-Type header is required", context, 400); if (!contentType.includes(CONTENT_TYPES.FORM_ENCODED)) return responseError(`Invalid Content-Type, expected ${CONTENT_TYPES.FORM_ENCODED}`, context, 415); const data = ProjectSchema.partial().parse(urlSearchParamsToObject(await request.formData())); const model = new ProjectModel(context, connectionString); await model.update(projectId, data); if (checkIsHTMLRequest() || checkIsHXRequest()) return responseRedirect(request.url, 303); return { status: 202, headers: { Location: request.url }, jsonBody: { data: await model.get(projectId), links: { self: request.url } } }; } catch (error) { return responseError(error, context, 404); } } async function deleteProject(request, context) { try { const { projectId } = request.params; if (!projectId) return { status: 400, body: "Missing project ID" }; const { connectionString } = getStore(); const model = new ProjectModel(context, connectionString); await model.delete(projectId); const projectsUrl = urlBuilder.allProjects(); if (checkIsHTMLRequest() || checkIsHXRequest()) return responseRedirect(projectsUrl, 303); return { status: 204, headers: { Location: projectsUrl } }; } catch (error) { return responseError(error, context, 404); } } //#endregion //#region src/routers/projects-router.ts const TAG = openAPITags.projects.name; function registerProjectsRouter(options) { const { authLevel, baseRoute, basePathParamsSchema, handlerWrapper, openAPIEnabled, serviceName } = options; const projectIdRoute = joinUrl(baseRoute, "{projectId}"); app.get(`${serviceName}-projects-list`, { authLevel, route: baseRoute, handler: handlerWrapper(listProjects, [{ resource: "project", action: "read" }, { resource: "ui", action: "read" }]) }); app.post(`${serviceName}-project-create`, { authLevel, route: baseRoute, handler: handlerWrapper(createProject, [{ resource: "project", action: "create" }]) }); app.get(`${serviceName}-project-get`, { authLevel, route: projectIdRoute, handler: handlerWrapper(getProject, [{ resource: "project", action: "read" }, { resource: "ui", action: "read" }]) }); app.patch(`${serviceName}-project-update`, { authLevel, route: projectIdRoute, handler: handlerWrapper(updateProject, [{ resource: "project", action: "update" }]) }); app.deleteRequest(`${serviceName}-project-delete`, { authLevel, route: projectIdRoute, handler: handlerWrapper(deleteProject, [{ resource: "project", action: "delete" }]) }); if (openAPIEnabled) { const projectPathParameterSchema = basePathParamsSchema.extend({ projectId: ProjectIdSchema }); registerOpenAPIPath(baseRoute, { get: { tags: [TAG], summary: "List all projects", description: "Retrieves a list of projects.", requestParams: { path: basePathParamsSchema }, responses: { ...commonErrorResponses, 200: { description: "A list of projects.", content: { [CONTENT_TYPES.JSON]: { schema: ProjectSchema.array(), example: [{ project: "project-id" }] }, [CONTENT_TYPES.HTML]: { example: "<!DOCTYPE html>" } } } } }, post: { tags: [TAG], summary: "Create a new project", description: "Creates a new project with the provided metadata.", requestBody: { required: true, description: "Data about the project", content: { [CONTENT_TYPES.FORM_ENCODED]: { schema: ProjectCreateSchema } } }, requestParams: { path: basePathParamsSchema }, responses: { ...commonErrorResponses, 201: { description: "Project created successfully", content: { [CONTENT_TYPES.JSON]: { schema: z.object({ data: ProjectSchema, links: z.object({ self: z.url() }) }) } } }, 303: { description: "Project created, redirecting...", headers: { Location: z.url() } }, 415: { description: "Unsupported Media Type" } } } }); registerOpenAPIPath(projectIdRoute, { get: { tags: [TAG], summary: "Get project details", description: "Retrieves the details of a specific project.", requestParams: { path: projectPathParameterSchema }, responses: { ...commonErrorResponses, 200: { description: "Project details retrieved successfully", content: { [CONTENT_TYPES.JSON]: { schema: ProjectSchema }, [CONTENT_TYPES.HTML]: { example: "<!DOCTYPE html>" } } }, 404: { description: "Matching project not found." } } }, patch: { tags: [TAG], summary: "Update project details", description: "Updates the details of a specific project.", requestParams: { path: projectPathParameterSchema }, requestBody: { required: true, description: "Updated project data", content: { [CONTENT_TYPES.FORM_ENCODED]: { schema: ProjectSchema.partial() } } }, responses: { ...commonErrorResponses, 202: { description: "Project updated successfully", content: { [CONTENT_TYPES.JSON]: { schema: z.object({ data: ProjectSchema, links: z.object({ self: z.url() }) }) } } }, 303: { description: "Project updated, redirecting...", headers: { Location: z.url() } }, 404: { description: "Matching project not found." }, 415: { description: "Unsupported Media Type" } } }, delete: { tags: [TAG], summary: "Delete a project", description: "Deletes a specific project.", requestParams: { path: projectPathParameterSchema }, responses: { ...commonErrorResponses, 204: { description: "Project deleted successfully" }, 404: { description: "Matching project not found." } } } }); } } //#endregion export { registerProjectsRouter }; //# sourceMappingURL=projects-router-B4T5MHdN.mjs.map