UNPKG

@azure-utils/storybooks

Version:

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

413 lines (409 loc) 14.3 kB
import { DocumentLayout, ErrorMessage, __toESM, checkIsHTMLRequest, checkIsHXRequest, checkIsNewMode, getStore, require_jsx_runtime, responseError, responseHTML, responseRedirect, urlBuilder } from "./store-BL4RNiEp.mjs"; import { CONTENT_TYPES, QUERY_PARAMS, commonErrorResponses } from "./constants-CsV1N9r4.mjs"; import { BuildSHASchema } from "./shared-BAE3ceND.mjs"; import { BuildModel, BuildSchema, BuildUploadFormSchema, BuildUploadSchema } from "./projects-DDVy3_77.mjs"; import { openAPITags, registerOpenAPIPath } from "./openapi-utils-CAJ85ahl.mjs"; import { joinUrl, urlSearchParamsToObject } from "./url-utils-B9Pl4bQ7.mjs"; import { BuildTable, RawDataPreview } from "./builds-table-D78yU-UW.mjs"; import { validateBuildUploadZipBody } from "./validators-C1FF8po_.mjs"; import { uploadZipWithDecompressed } from "./upload-utils-DHJHMiPY.mjs"; import { app } from "@azure/functions"; //#region src/components/build-form.tsx var import_jsx_runtime$1 = __toESM(require_jsx_runtime()); async function BuildForm({ projectId }) { return /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("form", { "hx-ext": "response-targets", "hx-post": urlBuilder.allBuilds(projectId), "hx-target-error": "#form-error", style: { maxWidth: "60ch" }, enctype: CONTENT_TYPES.FORM_MULTIPART, children: [ /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("fieldset", { children: [ /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("legend", { children: "Details" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { class: "field", children: [/* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("label", { for: "sha", children: "SHA" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("input", { id: "sha", name: "sha", required: true })] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { class: "field", children: [/* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("label", { for: "message", children: "Message" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("input", { id: "message", name: "message" })] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { class: "field", children: [/* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("label", { for: "zipFile", children: "Zip file" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("input", { id: "zipFile", name: "zipFile", type: "file", accept: CONTENT_TYPES.ZIP, required: true })] }) ] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("fieldset", { children: [ /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("legend", { children: "Author" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { class: "field", children: [/* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("label", { for: "authorName", children: "Name" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("input", { id: "authorName", name: "authorName", required: true })] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { class: "field", children: [/* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("label", { for: "authorEmail", children: "Email" }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("input", { id: "authorEmail", name: "authorEmail", required: true })] }) ] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("fieldset", { children: [/* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("legend", { children: "Labels" }), Array.from({ length: 4 }).map((_, i) => { const id = `label-${i}`; return /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { class: "field", children: [ /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("label", { for: id, children: ["Label ", i + 1] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("input", { id, name: "labels", required: i === 0 }), i === 0 ? /* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("span", { class: "description", children: "Required" }) : null ] }); })] }), /* @__PURE__ */ (0, import_jsx_runtime$1.jsxs)("div", { style: { display: "flex", gap: "1rem" }, children: [/* @__PURE__ */ (0, import_jsx_runtime$1.jsx)("button", { type: "submit", children: "Upload build" }), /* @__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/build-handlers.tsx var import_jsx_runtime = __toESM(require_jsx_runtime()); async function listBuilds(request, context) { const { projectId = "" } = request.params; try { if (checkIsNewMode()) return responseHTML(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DocumentLayout, { title: "Upload Build", breadcrumbs: [{ label: projectId, href: urlBuilder.projectId(projectId) }, { label: "Builds", href: urlBuilder.allBuilds(projectId) }], children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuildForm, { projectId }) })); const { connectionString } = getStore(); const buildModel = new BuildModel(context, connectionString, projectId); const builds = await buildModel.list(); if (checkIsHTMLRequest()) { const projectModel = buildModel.projectModel; const project = await projectModel.get(projectId); const labels = await projectModel.labelModel(projectId).list(); return responseHTML(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DocumentLayout, { title: "All Builds", breadcrumbs: [projectId], toolbar: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: urlBuilder.buildUpload(projectId), children: "Upload" }), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuildTable, { builds, labels, project, caption: `Builds (${builds.length})` }) })); } return { status: 200, jsonBody: builds }; } catch (error) { return responseError(error, context); } } async function getBuild(request, context) { const { projectId = "", buildSHA = "" } = request.params; const labelSlug = request.query.get(QUERY_PARAMS.labelSlug) || void 0; try { const { connectionString } = getStore(); const buildModel = new BuildModel(context, connectionString, projectId); const build = await buildModel.get(buildSHA, labelSlug); if (checkIsHTMLRequest()) return responseHTML(/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DocumentLayout, { breadcrumbs: [projectId, "Builds"], title: build.message ? `[${build.sha.slice(0, 7)}] ${build.message}` : buildSHA.slice(0, 7), toolbar: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { display: "flex", gap: "1rem", alignItems: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("form", { "hx-delete": request.url, "hx-confirm": "Are you sure about deleting the build?", 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: build, summary: "Build details", open: true }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: "1rem", display: "flex", gap: "1rem" }, children: [ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: urlBuilder.storybookIndexHtml(projectId, buildSHA), target: "_blank", children: "View Storybook" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: urlBuilder.storybookTestReport(projectId, buildSHA), target: "_blank", children: "View Test Report" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: urlBuilder.storybookCoverage(projectId, buildSHA), target: "_blank", children: "View Coverage" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: urlBuilder.storybookZip(projectId, buildSHA), download: `storybook-${projectId}-${buildSHA}.zip`, target: "_blank", children: "Download Storybook" }) ] })] }) })); return { status: 200, jsonBody: build }; } catch (error) { return responseError(error, context); } } async function deleteBuild(request, context) { const { projectId = "", buildSHA = "" } = request.params; try { const { connectionString } = getStore(); const buildModel = new BuildModel(context, connectionString, projectId); await buildModel.delete(buildSHA); const buildsUrl = urlBuilder.allBuilds(projectId); if (checkIsHTMLRequest() || checkIsHXRequest()) return responseRedirect(buildsUrl, 303); return { status: 204, headers: { Location: buildsUrl } }; } catch (error) { return responseError(error, context); } } async function uploadBuild(request, context) { try { const { projectId = "" } = request.params; const { connectionString } = getStore(); const buildModel = new BuildModel(context, connectionString, projectId); if (!await buildModel.projectModel.has(projectId)) return responseError(`The project '${projectId}' does not exist.`, context, 404); const contentType = request.headers.get("content-type"); if (contentType?.includes(CONTENT_TYPES.ZIP)) { const buildUploadData = BuildUploadSchema.parse(urlSearchParamsToObject(request.query)); const bodyValidationResponse = validateBuildUploadZipBody(request, context); if (bodyValidationResponse) return bodyValidationResponse; const uploadResponse = await uploadZipWithDecompressed(projectId, buildUploadData.sha); if (uploadResponse) return uploadResponse; await buildModel.create(buildUploadData); const buildUrl = urlBuilder.buildSHA(projectId, buildUploadData.sha); if (checkIsHTMLRequest() || checkIsHXRequest()) return responseRedirect(buildUrl, 303); return { status: 202 }; } if (contentType?.includes(CONTENT_TYPES.FORM_MULTIPART)) { const { zipFile,...buildUploadData } = BuildUploadFormSchema.parse(urlSearchParamsToObject(await request.formData())); const uploadResponse = await uploadZipWithDecompressed(projectId, buildUploadData.sha, zipFile); if (uploadResponse) return uploadResponse; await buildModel.create(buildUploadData); const buildUrl = urlBuilder.buildSHA(projectId, buildUploadData.sha); if (checkIsHTMLRequest() || checkIsHXRequest()) return responseRedirect(buildUrl, 303); return { status: 202 }; } return responseError(`Invalid content type, expected ${CONTENT_TYPES.ZIP} or ${CONTENT_TYPES.FORM_MULTIPART}.`, context, 415); } catch (error) { return responseError(error, context); } } //#endregion //#region src/routers/builds-router.ts const TAG = openAPITags.builds.name; function registerBuildsRouter(options) { const { authLevel, baseRoute, basePathParamsSchema, handlerWrapper, openAPIEnabled, serviceName } = options; const routeWithBuildSHA = joinUrl(baseRoute, "{buildSHA}"); app.get(`${serviceName}-builds-list`, { authLevel, route: baseRoute, handler: handlerWrapper(listBuilds, [{ resource: "build", action: "read" }, { resource: "ui", action: "read" }]) }); app.post(`${serviceName}-build-upload`, { authLevel, route: baseRoute, handler: handlerWrapper(uploadBuild, [{ resource: "build", action: "create" }]) }); app.get(`${serviceName}-build-get`, { authLevel, route: routeWithBuildSHA, handler: handlerWrapper(getBuild, [{ resource: "build", action: "read" }, { resource: "ui", action: "read" }]) }); app.deleteRequest(`${serviceName}-build-delete`, { authLevel, route: routeWithBuildSHA, handler: handlerWrapper(deleteBuild, [{ resource: "build", action: "delete" }]) }); if (openAPIEnabled) { const buildPathParameterSchema = basePathParamsSchema.extend({ buildSHA: BuildSHASchema }); registerOpenAPIPath(baseRoute, { get: { tags: [TAG], summary: "List all builds for the project.", description: "Retrieves a list of builds.", requestParams: { path: basePathParamsSchema }, responses: { ...commonErrorResponses, 200: { description: "A list of builds.", content: { [CONTENT_TYPES.JSON]: { schema: BuildSchema.array(), example: [{ project: "project-id", sha: "s123s14" }] }, [CONTENT_TYPES.HTML]: { example: "<!DOCTYPE html>" } } } } }, post: { tags: [TAG], summary: "Upload a new build", description: "Uploads a new build with the provided metadata.", requestParams: { path: basePathParamsSchema, query: BuildUploadSchema }, requestBody: { required: true, description: "Compressed zip containing storybook and test results.", content: { [CONTENT_TYPES.ZIP]: { schema: { type: "string", format: "binary" }, example: "storybook.zip" } } }, responses: { ...commonErrorResponses, 202: { description: "Build uploaded successfully" } } } }); registerOpenAPIPath(routeWithBuildSHA, { get: { tags: [TAG], summary: "Get build details", description: "Retrieves the details of a specific build.", requestParams: { path: buildPathParameterSchema }, responses: { ...commonErrorResponses, 200: { description: "Build details retrieved successfully", content: { [CONTENT_TYPES.JSON]: { schema: BuildSchema }, [CONTENT_TYPES.HTML]: { example: "<!DOCTYPE html>" } } }, 404: { description: "Matching build not found." } } }, delete: { tags: [TAG], summary: "Delete a build", description: "Deletes a specific build.", requestParams: { path: buildPathParameterSchema }, responses: { ...commonErrorResponses, 204: { description: "Build deleted successfully" }, 404: { description: "Matching build not found." } } } }); } } //#endregion export { registerBuildsRouter }; //# sourceMappingURL=builds-router-BvRICvOx.mjs.map