feat(pipelines): implement pipelines views (stage / logs)@@ -1,5 +1,5 @@
{
- "_generatedAtUnix": 1779126421985,
+ "_generatedAtUnix": 1779139833816,
"_hashAlgorithm": "sha1",
"_version": 2,
"assets": {
@@ -22,7 +22,7 @@
"pathSourceMap": "./public/.islands/AppRouter.bundle.js.map"
},
"Code": {
- "hash": "4243c08b20c3b66f9613648381e0601f0003c836",
+ "hash": "0536f41cc4d3e3a3245413af7682c26da63384a4",
"pathSource": "./app/islands/Code.tsx",
"pathBundle": "./public/.islands/Code.bundle.js",
"pathSourceMap": "./public/.islands/Code.bundle.js.map"
@@ -70,7 +70,7 @@
"pathSourceMap": "./public/.islands/RepositoryForkForm.bundle.js.map"
},
"RepositoryHero": {
- "hash": "fe354bc526c8e8ea82f257e2b056d0bcffdc4d20",
+ "hash": "b3b123dba534f69ac6fd54e99503226e2bb4328f",
"pathSource": "./app/islands/RepositoryHero.tsx",
"pathBundle": "./public/.islands/RepositoryHero.bundle.js",
"pathSourceMap": "./public/.islands/RepositoryHero.bundle.js.map"
@@ -126,11 +126,11 @@
"pathSource": "./app/views/pipelines/PipelineArtefactsView.tsx"
},
"PipelineDetailsView": {
- "hash": "3d5cfe3a4cdb93a153f894e4e452ba01708acff5",
+ "hash": "e02d2e36e1073d13cf2f563850690645aa2efa2d",
"pathSource": "./app/views/pipelines/PipelineDetailsView.tsx"
},
"PipelineStageDetailsView": {
- "hash": "dfae473d54628e39af86f2e8a53dea541b2e0391",
+ "hash": "ea9da43602fe85435dd58c9386e7d2d417662944",
"pathSource": "./app/views/pipelines/PipelineStageDetailsView.tsx"
},
"PipelineStagesView": {
@@ -138,19 +138,19 @@
"pathSource": "./app/views/pipelines/PipelineStagesView.tsx"
},
"PipelinesView": {
- "hash": "b0b0751c7a869918310db58db9441ac2380fea97",
+ "hash": "b3c337b3baa004c75c223fe271f8a93642f0d907",
"pathSource": "./app/views/pipelines/PipelinesView.tsx"
},
"RepositoryBrowserView": {
- "hash": "8ad5b2c67b6a65a04c2976cba9079ec2f0da1eee",
+ "hash": "f2f2af5bd2ab4cb6442f3ccb8ecbeda1e28c7da8",
"pathSource": "./app/views/repository/RepositoryBrowserView.tsx"
},
"RepositoryCommitsLogView": {
- "hash": "fee84b81eded32f58b010fa66941d9df1860fa8b",
+ "hash": "1431635552e8c48897efb3903106936fd7900ac9",
"pathSource": "./app/views/repository/RepositoryCommitsLogView.tsx"
},
"RepositoryCompareView": {
- "hash": "f88489e021baae8b3e55e3b4dbe2c3e4b0af8a29",
+ "hash": "8b4bf2a7b9d95dbd942d5ee42e3baed31ec23fd6",
"pathSource": "./app/views/repository/RepositoryCompareView.tsx"
},
"RepositoryCreateView": {
@@ -158,7 +158,7 @@
"pathSource": "./app/views/repository/RepositoryCreateView.tsx"
},
"RepositoryDetailsView": {
- "hash": "63df2df0c5ed9838505df2c351aece6e198d17bb",
+ "hash": "f599436ec5a019ce977ca4cddc61ebbff56588e3",
"pathSource": "./app/views/repository/RepositoryDetailsView.tsx"
},
"RepositoryExploreView": {
@@ -166,23 +166,23 @@
"pathSource": "./app/views/repository/RepositoryExploreView.tsx"
},
"RepositoryForkView": {
- "hash": "6c4f238f98aace40dfa8eeff9749d3841349f8e0",
+ "hash": "d991907250e5eb09943b8fee9e61567b3274cf0f",
"pathSource": "./app/views/repository/RepositoryForkView.tsx"
},
"RepositoryShowObjectView": {
- "hash": "3c5578449f0a838f9a8b679ec2360a212f5a4e4c",
+ "hash": "5f4d8f29fe7fbbcb9c2e60e398372da553abcaf8",
"pathSource": "./app/views/repository/RepositoryShowObjectView.tsx"
},
"RepositoryPullRequestCreateView": {
- "hash": "f5c39bf5a0ea9ec03a1a684293c7a1f595176c0b",
+ "hash": "e2d88584ca1b78c466d2df6379396c7e3be5c0f8",
"pathSource": "./app/views/repositoryPullRequests/RepositoryPullRequestCreateView.tsx"
},
"RepositoryPullRequestDetailsView": {
- "hash": "9664f6328191169ea5926e23fd63da11be4163c6",
+ "hash": "4f7629d5d1e75b53ce86e245c211526f5d63823f",
"pathSource": "./app/views/repositoryPullRequests/RepositoryPullRequestDetailsView.tsx"
},
"RepositoryPullRequestsView": {
- "hash": "6a5fb0057478583375f6806704ec8206e93f86a5",
+ "hash": "928cc5f89e5411f7e7354c119754b8166431c0b4",
"pathSource": "./app/views/repositoryPullRequests/RepositoryPullRequestsView.tsx"
},
"SettingsKeyAddView": {
@@ -198,7 +198,7 @@
"pathSource": "./app/views/settings/SettingsKeysListView.tsx"
},
"SettingsView": {
- "hash": "4d1432779c2a5ddf58e220599cc4042320a16eb8",
+ "hash": "8ce5cced9f229ab999f9e05cd62a82f4c5681285",
"pathSource": "./app/views/settings/SettingsView.tsx"
},
"UserDashboardView": {
@@ -206,7 +206,7 @@
"pathSource": "./app/views/user/UserDashboardView.tsx"
},
"UserDetailsView": {
- "hash": "cdf50c6716c65f78a3d01e834f4f8999c85529ad",
+ "hash": "bf11c0105a7fae171bb18fd12e38b207b2fd39c5",
"pathSource": "./app/views/user/UserDetailsView.tsx"
}
}
@@ -21,6 +21,7 @@ interface CodeProps {
code: string;
language: string;
style?: CSSProperties;
+ whiteSpace?: "pre-wrap" | "pre" | "normal" | "nowrap";
[x: string]: unknown;
}
@@ -48,6 +49,7 @@ const Code: ReactIsland<CodeProps & WithThemeSchemeProp> = ({
language,
themeScheme,
style,
+ whiteSpace,
...props
}) => {
const { before: lineStartAt } = useMemo(
@@ -176,7 +178,7 @@ const Code: ReactIsland<CodeProps & WithThemeSchemeProp> = ({
data-start={lineStartAt}
className={` language-${language} line-numbers`}
themeScheme={themeScheme}
- style={{ counterReset: "linenumber 0", ...(style || {}) }}
+ style={{ counterReset: "linenumber 0", ...(style || {}), whiteSpace }}
>
<StyledCodeTag {...props} dangerouslySetInnerHTML={innerHtml} />
</StylePreTag>
@@ -53,121 +53,126 @@ const RepositoryHero: ReactIsland<
return (
<Grid.Col
fluid
- gap={16}
style={{
+ margin: "0 -16px",
+ padding: "12px 16px",
+ width: "calc(100% + 32px)",
+ background: NamedColors.HEADER[themeScheme],
borderBottom: `1px solid ${NamedColors.BORDER_DEFAULT[themeScheme]}`,
}}
>
- <Grid.Row fluid nowrap alignItems={"flex-start"}>
- <StyledActionIconButtonAnchor
- themeScheme={themeScheme}
- onClick={
- typeof window !== "undefined"
- ? () => window.history.back()
- : undefined
- }
- visible
- >
- <BackIcon color={NamedColors.TEXT_DEFAULT[themeScheme]} size={24} />
- </StyledActionIconButtonAnchor>
- <Grid.Col
- nowrap
- flex={"1 0 300px"}
- style={{ minWidth: 300, marginBottom: 12, marginLeft: 12 }}
- >
- <h2 style={{ margin: 0, fontSize: 20 }}>
- <a
- style={{ whiteSpace: "nowrap" }}
- href={buildRouteLink(AppRoute.ORGANIZATION_DETAILS, {
- orgSlug: parentOrg.slug,
- })}
- >
- {parentOrg.displayName || parentOrg.slug}
- </a>
- {" / "}
- <a
- style={{ whiteSpace: "nowrap" }}
- href={buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
+ <Grid.Col fluid gap={16}>
+ <Grid.Row fluid nowrap alignItems={"center"}>
+ <StyledActionIconButtonAnchor
+ themeScheme={themeScheme}
+ onClick={
+ typeof window !== "undefined"
+ ? () => window.history.back()
+ : undefined
+ }
+ visible
+ >
+ <BackIcon color={NamedColors.TEXT_DEFAULT[themeScheme]} size={24} />
+ </StyledActionIconButtonAnchor>
+ <Grid.Col
+ nowrap
+ flex={"1 0 270px"}
+ style={{ minWidth: 270, marginLeft: 12 }}
+ >
+ <h2 style={{ margin: 0, fontSize: 20 }}>
+ <a
+ style={{ whiteSpace: "nowrap" }}
+ href={buildRouteLink(AppRoute.ORGANIZATION_DETAILS, {
+ orgSlug: parentOrg.slug,
+ })}
+ >
+ {parentOrg.displayName || parentOrg.slug}
+ </a>
+ {" / "}
+ <a
+ style={{ whiteSpace: "nowrap" }}
+ href={buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
+ orgSlug: parentOrg.slug,
+ repoSlug: repo.slug,
+ })}
+ >
+ {repo.displayName || repo.slug}
+ </a>
+ </h2>
+ <div style={{ flex: 1 }}>
+ <h3
+ style={{
+ whiteSpace: "nowrap",
+ margin: 0,
+ marginTop: 4,
+ fontSize: 16,
+ fontWeight: "700",
+ fontFamily: "monospace",
+ }}
+ >
+ {path || "Files"}
+ </h3>
+ </div>
+ <div style={{ flex: 1 }}>
+ {repo.isFork && forkedFromRepo != null && (
+ <h5 style={{ margin: 0, marginTop: 8 }}>
+ <span>Forked From</span>
+ {" ∙ "}
+ <a
+ href={buildRouteLink(AppRoute.ORGANIZATION_DETAILS, {
+ orgSlug: forkedFromRepo.organization.slug,
+ })}
+ >
+ {forkedFromRepo.organization.displayName ||
+ forkedFromRepo.organization.slug}
+ </a>
+ {" / "}
+ <a
+ href={buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
+ orgSlug: forkedFromRepo.organization.slug,
+ repoSlug: forkedFromRepo.slug,
+ })}
+ >
+ {forkedFromRepo.displayName || forkedFromRepo.slug}
+ </a>
+ </h5>
+ )}
+ </div>
+ </Grid.Col>
+ {showForkButton && (
+ <ButtonAnchor
+ href={buildRouteLink(AppRoute.REPOSITORY_FORK, {
orgSlug: parentOrg.slug,
repoSlug: repo.slug,
})}
>
- {repo.displayName || repo.slug}
- </a>
- </h2>
- <div style={{ flex: 1 }}>
- <h3
- style={{
- whiteSpace: "nowrap",
- margin: 0,
- marginTop: 4,
- fontSize: 16,
- fontWeight: "700",
- fontFamily: "monospace",
- }}
- >
- {path || "Files"}
- </h3>
- </div>
- <div style={{ flex: 1 }}>
- {repo.isFork && forkedFromRepo != null && (
- <h5 style={{ margin: 0, marginTop: 8 }}>
- <span>Forked From</span>
- {" ∙ "}
- <a
- href={buildRouteLink(AppRoute.ORGANIZATION_DETAILS, {
- orgSlug: forkedFromRepo.organization.slug,
- })}
- >
- {forkedFromRepo.organization.displayName ||
- forkedFromRepo.organization.slug}
- </a>
- {" / "}
- <a
- href={buildRouteLink(AppRoute.REPOSITORY_DETAILS, {
- orgSlug: forkedFromRepo.organization.slug,
- repoSlug: forkedFromRepo.slug,
- })}
- >
- {forkedFromRepo.displayName || forkedFromRepo.slug}
- </a>
- </h5>
- )}
- </div>
- </Grid.Col>
- {showForkButton && (
- <ButtonAnchor
- href={buildRouteLink(AppRoute.REPOSITORY_FORK, {
- orgSlug: parentOrg.slug,
- repoSlug: repo.slug,
- })}
- >
- <Grid.Row
- nowrap
- justifyContent={"center"}
- alignItems={"center"}
- gap={8}
- >
- <GitForkIcon color={Colors.WHITE_01} size={24} />
- <span>Fork</span>
- <span
- style={{
- padding: "2px 6px",
- minWidth: 20,
- background: "rgba(0,0,0,0.1)",
- borderRadius: 8,
- fontSize: 12,
- }}
+ <Grid.Row
+ nowrap
+ justifyContent={"center"}
+ alignItems={"center"}
+ gap={8}
>
- {forksCount}
- </span>
- </Grid.Row>
- </ButtonAnchor>
- )}
- {showNewButton && (
- <ButtonAnchor href={newButtonUrl}>{newButtonText}</ButtonAnchor>
- )}
- </Grid.Row>
+ <GitForkIcon color={Colors.WHITE_01} size={24} />
+ <span>Fork</span>
+ <span
+ style={{
+ padding: "2px 6px",
+ minWidth: 20,
+ background: "rgba(0,0,0,0.1)",
+ borderRadius: 8,
+ fontSize: 12,
+ }}
+ >
+ {forksCount}
+ </span>
+ </Grid.Row>
+ </ButtonAnchor>
+ )}
+ {showNewButton && (
+ <ButtonAnchor href={newButtonUrl}>{newButtonText}</ButtonAnchor>
+ )}
+ </Grid.Row>
+ </Grid.Col>
</Grid.Col>
);
};
@@ -1,20 +1,50 @@
// 1st-party
import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
// auto-generated via script [prisma:generate]
-import type { Pipeline } from "@prisma/client";
+import type {
+ Artefact,
+ Pipeline,
+ Repository,
+ Stage,
+ User,
+} from "@prisma/client";
// app
import type { PipelineServiceDeps } from "./types";
const makeGetPipeline: ServiceMethodFactory<
PipelineServiceDeps,
[string],
- Promise<Pipeline | null>
+ Promise<
+ | (Pipeline & {
+ stages: Stage[];
+ triggeredByUser: Omit<User, "hashedPassword" | "email"> | null;
+ artefacts: Artefact[];
+ repo: Repository;
+ })
+ | null
+ >
> = ({ request }) => {
return async (pipelineId: string) => {
return request.prisma.pipeline.findUnique({
where: {
id: pipelineId,
},
+ include: {
+ stages: true,
+ triggeredByUser: {
+ select: {
+ id: true,
+ username: true,
+ createdAt: true,
+ updatedAt: true,
+ avatarUri: true,
+ displayName: true,
+ role: true,
+ },
+ },
+ artefacts: true,
+ repo: true,
+ },
});
};
};
@@ -1,12 +1,26 @@
// 1st-party
import type { ServiceMethodFactory } from "@ethicdevs/react-monolith";
-import { Pipeline, PipelineStatus } from "@prisma/client";
+import {
+ Artefact,
+ Pipeline,
+ PipelineStatus,
+ Repository,
+ Stage,
+ User,
+} from "@prisma/client";
import type { PipelineServiceDeps, PipelinesFilter } from "./types";
const makeListByRepo: ServiceMethodFactory<
PipelineServiceDeps,
[string, string],
- Promise<Pipeline[]>
+ Promise<
+ (Pipeline & {
+ stages: Stage[];
+ triggeredByUser: Omit<User, "hashedPassword" | "email"> | null;
+ artefacts: Artefact[];
+ repo: Repository;
+ })[]
+ >
> = ({ request }) => {
return async (
orgSlug: string,
@@ -33,6 +47,22 @@ const makeListByRepo: ServiceMethodFactory<
: undefined,
},
orderBy: { createdAt: "desc" },
+ include: {
+ stages: true,
+ triggeredByUser: {
+ select: {
+ id: true,
+ username: true,
+ avatarUri: true,
+ createdAt: true,
+ updatedAt: true,
+ displayName: true,
+ role: true,
+ },
+ },
+ artefacts: true,
+ repo: true,
+ },
});
};
};
@@ -1,7 +1,13 @@
// 3rd-party
import type { FastifyRequest } from "fastify";
// generated via script [prisma:generate]
-import type { Artefact, Pipeline, Stage } from "@prisma/client";
+import type {
+ Artefact,
+ Pipeline,
+ Repository,
+ Stage,
+ User,
+} from "@prisma/client";
// Lightweight shared types for pipeline domain to minimize coupling in MVP.
export type Manifest = { manifest: string; version?: string };
@@ -28,7 +34,14 @@ export type PipelineServiceAPI = {
orgSlug: string,
repoSlug: string,
filter?: PipelinesFilter,
- ): Promise<Pipeline[]>;
+ ): Promise<
+ (Pipeline & {
+ stages: Stage[];
+ triggeredByUser: Omit<User, "hashedPassword" | "email"> | null;
+ artefacts: Artefact[];
+ repo: Repository;
+ })[]
+ >;
listByRepoId(repoId: string): Promise<Pipeline[]>;
getRepoPipelineManifest(orgSlug: string, repoSlug: string): Promise<Manifest>;
parsePipelineManifest(manifestJsonOrYml: string): Manifest;
@@ -39,7 +52,14 @@ export type PipelineServiceAPI = {
stageId: string,
): Promise<Stage["logs"][]>;
getPipelineArtefacts(pipelineId: string): Promise<Artefact[]>;
- getPipeline(pipelineId: string): Promise<Pipeline>;
+ getPipeline(pipelineId: string): Promise<
+ Pipeline & {
+ stages: Stage[];
+ triggeredByUser: Omit<User, "hashedPassword" | "email"> | null;
+ artefacts: Artefact[];
+ repo: Repository;
+ }
+ >;
setPipeline(pipelineId: string, data: Partial<Pipeline>): Promise<Pipeline>;
rmPipeline(pipelineId: string, reason: string): Promise<Pipeline>;
initRunnerForRepo(
@@ -3,14 +3,24 @@ import type { ReactView } from "@ethicdevs/react-monolith";
// 3rd-party
import React from "react";
// generated via script[prisma:generate]
-import type { Pipeline } from "@prisma/client";
+import type { Pipeline, Stage } from "@prisma/client";
// app
import type { CommonProps } from "../../types";
import { Layout } from "../../components/Layout";
import { PageWrapper } from "../../components/PageWrapper";
+import { Grid } from "../../components/Grid";
+import { Card } from "../../components/Card.styled";
+import { Chip } from "../../components/Chip";
+import { IslandWrapper } from "../../components/IslandWrapper.styled";
+import { Colors, NamedColors } from "../../utils/style";
+import { AppRoute } from "../../routes.defs";
+import { buildRouteLink } from "../../utils/shared";
+// app islands
+import Code from "../../islands/Code";
+import RepositoryHero from "../../islands/RepositoryHero";
export interface PipelineDetailsViewProps extends CommonProps {
- pipeline: Pipeline;
+ pipeline: Pipeline & { stages: Stage[] };
orgSlug: string;
repoSlug: string;
}
@@ -22,10 +32,178 @@ const PipelineDetailsView: ReactView<PipelineDetailsViewProps> = ({
repoSlug,
}) => {
return (
- <Layout {...commonProps} orgSlug={orgSlug} repoSlug={repoSlug}>
- <PageWrapper>
- <h2>Pipeline: {pipeline.name ?? pipeline.id}</h2>
- <p>Status: {pipeline.status ?? "unknown"}</p>
+ <Layout
+ {...commonProps}
+ showDrawerPrimary
+ orgSlug={orgSlug}
+ repoSlug={repoSlug}
+ >
+ <PageWrapper style={{ gap: 16 }}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
+ <RepositoryHero
+ themeScheme={commonProps.themeScheme}
+ // forkedFromRepo={repo.forkedFromRepo}
+ // forksCount={repo.forks.length}
+ // parentOrg={parentOrg}
+ // repo={repo}
+ path={`Pipeline > #${pipeline.name ?? "Unknown"}`}
+ showForkButton={false}
+ showNewButton
+ newButtonText={"Download Artifacts"}
+ newButtonUrl={buildRouteLink(
+ AppRoute.REPOSITORY_PIPELINE_ARTEFACTS,
+ {
+ orgSlug: orgSlug,
+ repoSlug: repoSlug,
+ pipelineId: pipeline.id,
+ },
+ )}
+ parentOrg={{
+ id: "",
+ createdAt: new Date(Date.now()),
+ updatedAt: new Date(Date.now()),
+ slug: orgSlug,
+ ownerId: "",
+ kind: "PERSONAL",
+ visibility: "PUBLIC",
+ avatarUri: null,
+ displayName: orgSlug,
+ websiteUrl: null,
+ }}
+ repo={{
+ id: "",
+ forkedFromRepoId: null,
+ organizationId: "",
+ slug: repoSlug,
+ createdAt: new Date(Date.now()),
+ updatedAt: new Date(Date.now()),
+ lastPushedAt: null,
+ visibility: "PUBLIC",
+ isFork: false,
+ keywords: [],
+ avatarUri: null,
+ displayName: repoSlug,
+ shortDescription: null,
+ websiteUrl: null,
+ }}
+ />
+ </IslandWrapper>
+ <Grid.Row fluid nowrap gap={8} alignItems={"center"}>
+ <Chip
+ themeScheme={commonProps.themeScheme}
+ color={
+ {
+ PENDING: Colors.PRIMARY_01,
+ RUNNING: Colors.CYAN_01,
+ PASSED: Colors.GREEN_01,
+ FAILED: Colors.RED_01,
+ CANCELED: Colors.BLACK_01,
+ }[pipeline.status]
+ }
+ >
+ {
+ {
+ PENDING: "Pending",
+ RUNNING: "Running",
+ PASSED: "Passed",
+ FAILED: "Failed",
+ CANCELED: "Canceled",
+ }[pipeline.status]
+ }
+ </Chip>
+ </Grid.Row>
+ <Grid.Row fluid gap={4} alignItems={"flex-start"}>
+ {pipeline.stages
+ .sort((a, b) => a.order - b.order)
+ .map((stage) => (
+ <Card
+ themeScheme={commonProps.themeScheme}
+ style={{ flex: 1, padding: 4 }}
+ >
+ <Grid.Col key={stage.id} fluid gap={4}>
+ <h3 style={{ margin: 0 }}>
+ <a
+ href={buildRouteLink(
+ AppRoute.REPOSITORY_PIPELINE_STAGE_DETAILS,
+ {
+ orgSlug,
+ repoSlug,
+ pipelineId: pipeline.id,
+ stageId: stage.id,
+ },
+ )}
+ >
+ {stage.name ?? stage.id}
+ </a>
+ </h3>
+ <Grid.Row fluid nowrap gap={4} alignItems={"center"}>
+ <div
+ style={{
+ fontSize: 11,
+ color: NamedColors.TEXT_MUTED[commonProps.themeScheme],
+ }}
+ >
+ #{stage.order}
+ </div>
+ <div
+ style={{
+ fontSize: 11,
+ color: NamedColors.TEXT_MUTED[commonProps.themeScheme],
+ }}
+ >
+ {stage.updatedAt.toLocaleString()}
+ </div>
+ </Grid.Row>
+ <Chip
+ themeScheme={commonProps.themeScheme}
+ color={
+ {
+ PENDING: Colors.PRIMARY_01,
+ RUNNING: Colors.CYAN_01,
+ PASSED: Colors.GREEN_01,
+ FAILED: Colors.RED_01,
+ CANCELED: Colors.BLACK_01,
+ }[stage.status]
+ }
+ >
+ {
+ {
+ PENDING: "Pending",
+ RUNNING: "Running",
+ PASSED: "Passed",
+ FAILED: "Failed",
+ CANCELED: "Canceled",
+ }[stage.status]
+ }
+ </Chip>
+ {/*<code>{stage.logs}</code>*/}
+ </Grid.Col>
+ </Card>
+ ))}
+ </Grid.Row>
+ {pipeline.manifest && (
+ <Grid.Col fluid nowrap gap={4}>
+ <IslandWrapper data-islandid={`${Code.name}$$0`}>
+ <Code
+ themeScheme={commonProps.themeScheme}
+ language={"shell"}
+ code={`${orgSlug}/${repoSlug}/.gitfoss.ci`}
+ whiteSpace={"pre-wrap"}
+ />
+ </IslandWrapper>
+ <IslandWrapper data-islandid={`${Code.name}$$1`}>
+ <Code
+ themeScheme={commonProps.themeScheme}
+ language={"yaml"}
+ code={pipeline.manifest}
+ whiteSpace={"pre-wrap"}
+ />
+ </IslandWrapper>
+ </Grid.Col>
+ )}
</PageWrapper>
</Layout>
);
@@ -8,28 +8,103 @@ import type { Pipeline, Stage } from "@prisma/client";
import type { CommonProps } from "../../types";
import { Layout } from "../../components/Layout";
import { PageWrapper } from "../../components/PageWrapper";
+import { IslandWrapper } from "../../components/IslandWrapper.styled";
+import { AppRoute } from "../../routes.defs";
+import { buildRouteLink } from "../../utils/shared";
+// app islands
+import Code from "../../islands/Code";
+import RepositoryHero from "../../islands/RepositoryHero";
export interface PipelineStageDetailsViewProps extends CommonProps {
pipeline: Pipeline;
stage: Stage | null;
- logs: Stage["logs"][];
+ logs: Stage["logs"];
orgSlug: string;
repoSlug: string;
}
const PipelineStageDetailsView: ReactView<PipelineStageDetailsViewProps> = ({
commonProps,
- // pipeline,
+ pipeline,
stage,
logs,
orgSlug,
repoSlug,
}) => {
return (
- <Layout {...commonProps} orgSlug={orgSlug} repoSlug={repoSlug}>
- <PageWrapper>
- <h2>Stage: {stage?.name ?? stage?.id ?? "Unknown"}</h2>
- <div>Logs: {logs?.length ?? 0}</div>
+ <Layout
+ {...commonProps}
+ showDrawerPrimary
+ orgSlug={orgSlug}
+ repoSlug={repoSlug}
+ >
+ <PageWrapper style={{ gap: 8 }}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
+ <RepositoryHero
+ themeScheme={commonProps.themeScheme}
+ // forkedFromRepo={repo.forkedFromRepo}
+ // forksCount={repo.forks.length}
+ // parentOrg={parentOrg}
+ // repo={repo}
+ path={`Pipeline > ${pipeline.name ?? "Unknown"} > #${stage?.order ?? 0} - ${stage?.name ?? "Unknown"}`}
+ showForkButton={false}
+ showNewButton
+ newButtonText={"Download Artifacts"}
+ newButtonUrl={buildRouteLink(
+ AppRoute.REPOSITORY_PIPELINE_ARTEFACTS,
+ {
+ orgSlug: orgSlug,
+ repoSlug: repoSlug,
+ pipelineId: pipeline.id,
+ },
+ )}
+ parentOrg={{
+ id: "",
+ createdAt: new Date(Date.now()),
+ updatedAt: new Date(Date.now()),
+ slug: orgSlug,
+ ownerId: "",
+ kind: "PERSONAL",
+ visibility: "PUBLIC",
+ avatarUri: null,
+ displayName: orgSlug,
+ websiteUrl: null,
+ }}
+ repo={{
+ id: "",
+ forkedFromRepoId: null,
+ organizationId: "",
+ slug: repoSlug,
+ createdAt: new Date(Date.now()),
+ updatedAt: new Date(Date.now()),
+ lastPushedAt: null,
+ visibility: "PUBLIC",
+ isFork: false,
+ keywords: [],
+ avatarUri: null,
+ displayName: repoSlug,
+ shortDescription: null,
+ websiteUrl: null,
+ }}
+ />
+ </IslandWrapper>
+ <IslandWrapper data-islandid={`${Code.name}$$0`}>
+ <Code
+ themeScheme={commonProps.themeScheme}
+ code={logs || ""}
+ language={"shell"}
+ whiteSpace={"pre-wrap"}
+ style={{
+ minHeight: "70vh",
+ overflowY: "scroll",
+ // backgroundColor: "black !important",
+ // color: "white",
+ }}
+ />
+ </IslandWrapper>
</PageWrapper>
</Layout>
);
@@ -3,7 +3,14 @@ import type { ReactView } from "@ethicdevs/react-monolith";
// 3rd-party
import React from "react";
// generated via script[generate:prisma]
-import type { Organization, Pipeline } from "@prisma/client";
+import type {
+ Artefact,
+ Organization,
+ Pipeline,
+ Repository,
+ Stage,
+ User,
+} from "@prisma/client";
// app
import type { CommonProps, RepositoryWithForkedFromRepo } from "../../types";
import { AppRoute } from "../../routes.defs";
@@ -14,7 +21,7 @@ import { Grid } from "../../components/Grid";
import { Layout } from "../../components/Layout";
import { PageWrapper } from "../../components/PageWrapper";
import { buildRouteLink } from "../../utils/shared";
-import { Colors } from "../../utils/style";
+import { Colors, NamedColors } from "../../utils/style";
// app islands
import { IslandWrapper } from "../../components/IslandWrapper.styled";
import RepositoryHero from "../../islands/RepositoryHero";
@@ -29,7 +36,12 @@ type PipelinesFilter =
export interface PipelinesViewProps extends CommonProps {
parentOrg: Organization;
- pipelines: Pipeline[];
+ pipelines: (Pipeline & {
+ stages: Stage[];
+ triggeredByUser: Omit<User, "hashedPassword" | "email"> | null;
+ artefacts: Artefact[];
+ repo: Repository;
+ })[];
repo: RepositoryWithForkedFromRepo;
pipelinesFilter?: PipelinesFilter;
}
@@ -49,7 +61,10 @@ const PipelinesView: ReactView<PipelinesViewProps> = ({
repoSlug={repo.slug}
>
<PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
<RepositoryHero
themeScheme={commonProps.themeScheme}
forkedFromRepo={repo.forkedFromRepo}
@@ -164,9 +179,17 @@ const PipelinesView: ReactView<PipelinesViewProps> = ({
})}
>
<Grid.Row fluid alignItems={"center"} gap={4}>
- <span>id: {pipeline.id}</span>
+ <span>{pipeline.name}</span>
<span>•</span>
- <span style={{ flex: 1 }}>{pipeline.name}</span>
+ <span
+ style={{
+ flex: 1,
+ color:
+ NamedColors.TEXT_MUTED[commonProps.themeScheme],
+ }}
+ >
+ {pipeline.id}
+ </span>
<Chip
themeScheme={commonProps.themeScheme}
color={
@@ -191,21 +214,44 @@ const PipelinesView: ReactView<PipelinesViewProps> = ({
</Chip>
</Grid.Row>
</a>
- <Grid.Row
+ {pipeline.triggeredByUser != null && (
+ <Grid.Row
+ fluid
+ nowrap
+ gap={8}
+ alignItems={"center"}
+ style={{ opacity: 0.67 }}
+ >
+ triggered by
+ <a
+ href={buildRouteLink(
+ AppRoute.USER_DETAILS,
+ { username: `@${pipeline.triggeredByUser.username}` },
+ { encodeURIComponent: false },
+ )}
+ style={{ textTransform: "none" }}
+ >
+ @{pipeline.triggeredByUser.username}
+ </a>
+ </Grid.Row>
+ )}
+ <Grid.Col
fluid
- nowrap
- gap={8}
- alignItems={"center"}
- style={{ opacity: 0.67 }}
+ gap={4}
+ style={{ opacity: 0.67, marginTop: 4 }}
>
- triggered by
- <Chip
- themeScheme={commonProps.themeScheme}
- style={{ textTransform: "none" }}
- >
- <code>{""}</code>
- </Chip>
- </Grid.Row>
+ {new Date(pipeline.createdAt).getTime() <=
+ new Date(pipeline.updatedAt).getTime() && (
+ <span>
+ {`opened on ${new Date(pipeline.createdAt).toLocaleString()}`}
+ </span>
+ )}
+ {pipeline.closedAt != null && (
+ <span>
+ {`closed on ${new Date(pipeline.closedAt).toLocaleString()}`}
+ </span>
+ )}
+ </Grid.Col>
</Grid.Col>
</Card>
))
@@ -66,7 +66,10 @@ const RepositoryBrowserView: ReactView<RepositoryBrowserViewProps> = ({
path={path}
>
<PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
<RepositoryHero
themeScheme={commonProps.themeScheme}
forkedFromRepo={repo.forkedFromRepo}
@@ -44,7 +44,10 @@ const RepositoryCommitsLogView: ReactView<RepositoryCommitsLogViewProps> = ({
currentRef={currentRef}
>
<PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
<RepositoryHero
themeScheme={commonProps.themeScheme}
forkedFromRepo={repo.forkedFromRepo}
@@ -41,7 +41,10 @@ const RepositoryCompareView: ReactView<RepositoryCompareViewProps> = ({
currentRef={refA}
>
<PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
<RepositoryHero
themeScheme={commonProps.themeScheme}
forkedFromRepo={repo.forkedFromRepo}
@@ -74,7 +74,10 @@ const RepositoryDetailsView: ReactView<RepositoryDetailsViewProps> = ({
currentRef={currentRef}
>
<PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
<RepositoryHero
themeScheme={commonProps.themeScheme}
forkedFromRepo={forkedFromRepo}
@@ -42,7 +42,10 @@ const RepositoryForkView: ReactView<RepositoryForkViewProps> = ({
repoSlug={sourceRepo.slug}
>
<PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
<RepositoryHero
themeScheme={commonProps.themeScheme}
forkedFromRepo={sourceRepo.forkedFromRepo}
@@ -59,7 +59,10 @@ const RepositoryShowObjectView: ReactView<RepositoryShowObjectViewProps> = ({
currentRef={currentRef}
>
<PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
<RepositoryHero
themeScheme={commonProps.themeScheme}
forkedFromRepo={repo.forkedFromRepo}
@@ -32,7 +32,10 @@ const RepositoryPullRequestCreateView: ReactView<
repoSlug={repo.slug}
>
<PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
<RepositoryHero
themeScheme={commonProps.themeScheme}
forkedFromRepo={repo.forkedFromRepo}
@@ -85,7 +85,10 @@ const RepositoryPullRequestDetailsView: ReactView<
repoSlug={repo.slug}
>
<PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
<RepositoryHero
themeScheme={commonProps.themeScheme}
forkedFromRepo={repo.forkedFromRepo}
@@ -45,7 +45,10 @@ const RepositoryPullRequestsView: ReactView<
repoSlug={repo.slug}
>
<PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
<RepositoryHero
themeScheme={commonProps.themeScheme}
forkedFromRepo={repo.forkedFromRepo}
@@ -32,7 +32,10 @@ const SettingsView: ReactView<SettingsViewProps> = ({
username={commonProps.currentUserUsername!}
>
<PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
<RepositoryHero
themeScheme={commonProps.themeScheme}
parentOrg={{ slug: commonProps.currentUserUsername! } as any}
@@ -30,7 +30,10 @@ const UserDetailsView: ReactView<UserDetailsViewProps> = ({
username={user.username!}
>
<PageWrapper>
- <IslandWrapper data-islandid={`${RepositoryHero.name}$$0`}>
+ <IslandWrapper
+ style={{ position: "sticky", top: 64 }}
+ data-islandid={`${RepositoryHero.name}$$0`}
+ >
<RepositoryHero
themeScheme={commonProps.themeScheme}
parentOrg={{ slug: user.username } as any}
@@ -0,0 +1,16 @@
+-- AlterTable
+ALTER TABLE "Artefact" ADD COLUMN "stageId" TEXT NOT NULL DEFAULT '';
+
+-- AlterTable
+ALTER TABLE "Pipeline" ADD COLUMN "closedAt" TIMESTAMP(3),
+ADD COLUMN "triggeredByUserId" TEXT;
+
+-- AlterTable
+ALTER TABLE "Stage" ADD COLUMN "closedAt" TIMESTAMP(3),
+ADD COLUMN "exitCode" INTEGER;
+
+-- AddForeignKey
+ALTER TABLE "Pipeline" ADD CONSTRAINT "Pipeline_triggeredByUserId_fkey" FOREIGN KEY ("triggeredByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Artefact" ADD CONSTRAINT "Artefact_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "Stage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -147,6 +147,7 @@ model User {
organizationMemberships OrganizationMembership[] @relation("OneOrganizationMembershipToOneUser")
pullRequestsWhereAuthor PullRequest[] @relation("OnePullRequestToOneUser")
pullRequestCommentsWhereAuthor PullRequestComment[] @relation("OnePullRequestToOnePRCommenterUser")
+ pipelinesWhereTriggeredByUser Pipeline[] @relation("PipelineTriggeredByUser")
}
model UserSSHKey {
@@ -166,33 +167,40 @@ model UserSSHKey {
}
model Pipeline {
- id String @id @default(cuid())
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- name String
- status PipelineStatus @default(PENDING)
- manifest String?
-
- repoId String
- repo Repository @relation("RepositoryPipelines",fields: [repoId], references: [id])
-
- stages Stage[] @relation("PipelineStages")
- artefacts Artefact[] @relation("PipelineArtefacts")
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ closedAt DateTime?
+
+ name String
+ status PipelineStatus @default(PENDING)
+ manifest String?
+ triggeredByUserId String?
+ triggeredByUser User? @relation("PipelineTriggeredByUser", fields: [triggeredByUserId], references: [id])
+
+ repoId String
+ repo Repository @relation("RepositoryPipelines", fields: [repoId], references: [id])
+
+ stages Stage[] @relation("PipelineStages")
+ artefacts Artefact[] @relation("PipelineArtefacts")
}
model Stage {
- id String @id @default(cuid())
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
+ id String @id @default(cuid())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ closedAt DateTime?
name String
order Int
status StageStatus @default(PENDING)
logs String?
+ exitCode Int?
pipelineId String
- pipeline Pipeline @relation("PipelineStages", fields: [pipelineId], references: [id])
+ pipeline Pipeline @relation("PipelineStages", fields: [pipelineId], references: [id])
+
+ artefacts Artefact[] @relation("PipelineStageArtefacts")
}
model Artefact {
@@ -205,6 +213,9 @@ model Artefact {
pipelineId String
pipeline Pipeline @relation("PipelineArtefacts", fields: [pipelineId], references: [id])
+
+ stageId String @default("")
+ stage Stage @relation("PipelineStageArtefacts", fields: [stageId], references: [id])
}
enum GlobalRole {