Compare commits

..

4 Commits

Author SHA1 Message Date
Ivan Li
282366cd72 feat(pipelines):新增 添加 pipeline 功能。 2021-03-07 00:51:56 +08:00
Ivan Li
846b58c49b feat(apollo): 接口报错时,界面提供反馈。 2021-03-06 23:05:44 +08:00
Ivan Li
699a7cecd3 feat(message): 添加 消息提示模块。 2021-03-06 23:05:16 +08:00
Ivan Li
12621fae55 refactor: 改用路由处理多页面。 2021-03-06 15:25:08 +08:00
28 changed files with 1391 additions and 171 deletions

View File

@ -1,10 +1,6 @@
{
"css.validate": false,
"scss.validate": false,
"editor.quickSuggestions": {
"strings": true,
"other": true,
},
"cSpell.words": [
"Formik"
]

View File

@ -552,6 +552,354 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "WorkUnit",
"description": null,
"fields": [
{
"name": "type",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "PipelineUnits",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scripts",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "PipelineUnits",
"description": "流水线单元",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "checkout",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "installDependencies",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "test",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deploy",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "cleanUp",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "WorkUnitMetadata",
"description": null,
"fields": [
{
"name": "version",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Float",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "units",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "WorkUnit",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Pipeline",
"description": null,
"fields": [
{
"name": "id",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "project",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Project",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "projectId",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "branch",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "workUnitMetadata",
"description": null,
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "WorkUnitMetadata",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "WorkUnitInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "type",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "PipelineUnits",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scripts",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "WorkUnitMetadataInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "version",
"description": null,
"type": {
"kind": "SCALAR",
"name": "Float",
"ofType": null
},
"defaultValue": "1",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "units",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "WorkUnitInput",
"ofType": null
}
}
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Query",
@ -695,6 +1043,76 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "listPipelines",
"description": null,
"args": [
{
"name": "projectId",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Pipeline",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "findPipeline",
"description": null,
"args": [
{
"name": "id",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Pipeline",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
@ -889,18 +1307,100 @@
"deprecationReason": null
},
{
"name": "checkout",
"name": "createPipeline",
"description": null,
"args": [
{
"name": "checkoutInput",
"name": "pipeline",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "CheckoutInput",
"name": "CreatePipelineInput",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Pipeline",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "modifyPipeline",
"description": null,
"args": [
{
"name": "Pipeline",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdatePipelineInput",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Pipeline",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "deletePipeline",
"description": null,
"args": [
{
"name": "id",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
@ -914,7 +1414,7 @@
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"name": "Float",
"ofType": null
}
},
@ -1095,7 +1595,7 @@
},
{
"kind": "INPUT_OBJECT",
"name": "CheckoutInput",
"name": "CreatePipelineInput",
"description": null,
"fields": null,
"inputFields": [
@ -1119,21 +1619,120 @@
"name": "branch",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "commitNumber",
"name": "name",
"description": null,
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "workUnitMetadata",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "WorkUnitMetadataInput",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdatePipelineInput",
"description": null,
"fields": null,
"inputFields": [
{
"name": "projectId",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "branch",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "workUnitMetadata",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "WorkUnitMetadataInput",
"ofType": null
}
},
"defaultValue": null,
"isDeprecated": false,

View File

@ -1,15 +1,15 @@
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import { ApolloProvider } from '@apollo/client';
import { Fragment, FunctionalComponent, h } from 'preact';
import { ProjectPanel } from './projects/project-panel';
import styles from './app.scss';
import { OverlayContainer } from './commons/overlay/overlay';
import { useObserver } from 'mobx-react';
import { appStore } from '../app.store';
import Router, { Route } from 'preact-router';
import { ProjectDetails } from '../routes/projects/project-details';
import { createApolloClient } from '../units/apollo-client';
import { PipelineEditor } from '../routes/pipelines/pipeline-editor';
const client = new ApolloClient({
uri: '/api/graphql',
cache: new InMemoryCache()
});
const client = createApolloClient();
const App: FunctionalComponent = () => {
return (
@ -29,7 +29,15 @@ const Board = () => {
return useObserver(() => {
return (
<Fragment>
<main>{appStore.main}</main>
<main>
<Router>
<Route path="/projects/:id" component={ProjectDetails} />
<Route
path="/dev"
component={() => <PipelineEditor projectId="test" />}
/>
</Router>
</main>
<aside>
<ProjectPanel />
</aside>

View File

@ -1,10 +1,10 @@
import { h } from 'preact';
import styles from './commit-actions.scss';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { autorun, makeAutoObservable } from 'mobx';
import { makeAutoObservable } from 'mobx';
import { ActionButton } from './action-button';
import { gql, useMutation } from '@apollo/client';
import { CheckoutInput, Project } from '../../generated/graphql';
import { Project } from '../../generated/graphql';
import {
CommitActionsStoreProvider,
CommitActionsStore
@ -17,11 +17,7 @@ import {
} from '@fortawesome/free-solid-svg-icons';
import { useLocalObservable } from 'mobx-react';
const CHECKOUT = gql`
mutation Checkout($input: CheckoutInput!) {
checkout(checkoutInput: $input)
}
`;
const CHECKOUT = '';
interface Props {
project: Project;

View File

@ -1,5 +1,5 @@
import { FC } from 'preact/compat';
import { h } from 'preact';
import { h, RenderableProps } from 'preact';
interface Props {
loading?: boolean;

View File

@ -0,0 +1,2 @@
import { controller } from './messages-container';
export const Message = controller;

View File

@ -0,0 +1,13 @@
.message {
@apply px-4 py-2 rounded-xl bg-white dark:bg-gray-900;
}
.error {
@apply bg-red-200 text-red-600;
}
.success {
@apply bg-green-200 text-green-600;
}
.container {
@apply mt-5;
@apply grid grid-cols-1 gap-y-2 justify-items-center;
}

View File

@ -0,0 +1,14 @@
// This file is automatically generated from your CSS. Any edits will be overwritten.
declare namespace MessageScssNamespace {
export interface IMessageScss {
container: string;
dark: string;
error: string;
message: string;
success: string;
}
}
declare const MessageScssModule: MessageScssNamespace.IMessageScss;
export = MessageScssModule;

View File

@ -0,0 +1,25 @@
import styles from './message.scss';
import classNames from 'classnames';
import { h } from 'preact';
export class MessageOptions {
message = '';
type: 'success' | 'error' | 'default' = 'default';
/**
*
*/
duration = 5000;
}
export const MessageComponent = ({ message, type }: MessageOptions) => {
return (
<section
className={classNames(styles.message, {
[styles.success]: type === 'success',
[styles.error]: type === 'error'
})}
>
{message}
</section>
);
};

View File

@ -0,0 +1,66 @@
import { MessageComponent, MessageOptions } from './message';
import { action, autorun, computed, makeObservable, observable } from 'mobx';
import { h } from 'preact';
import { createOverlay } from '../overlay/overlay';
import { observer } from 'mobx-react';
import styles from './message.scss';
class Store {
@observable
messages: MessageOptions[] = [];
isNeedCreateView = true;
constructor() {
makeObservable(this);
autorun(() => {
if (this.messageComponents.length > 0 && this.isNeedCreateView) {
this.isNeedCreateView = false;
createOverlay({
content: <MessageContainer store={this} />,
mask: false
});
}
});
}
@computed
get messageComponents() {
return this.messages.map((options, index) => (
<MessageComponent {...options} key={index} />
));
}
@action
add(options: MessageOptions) {
this.messages.push(options);
setTimeout(() => {
const index = this.messages.indexOf(options);
if (index === -1) {
return;
}
this.messages.splice(index, 1);
}, options.duration);
}
}
interface Controller {
error: (message: string) => void;
success: (message: string) => void;
}
const store = new Store();
const addMessage = (options: Partial<MessageOptions & { message: string }>) => {
store.add(Object.assign(new MessageOptions(), options));
};
export const controller: Controller = {
error(message) {
return addMessage({ message, type: 'error' });
},
success(message) {
return addMessage({ message, type: 'success' });
}
};
const MessageContainer = observer(({ store }: { store: Store }) => {
return <ol className={styles.container}>{store.messageComponents}</ol>;
});

View File

@ -2,5 +2,8 @@
@apply opacity-50 bg-white fixed top-0 bottom-0 left-0 right-0;
}
.body {
@apply fixed top-0 bottom-0 left-0 right-0;
@apply fixed top-0 bottom-0 left-0 right-0 pointer-events-none;
& > * {
@apply pointer-events-auto;
}
}

View File

@ -9,6 +9,7 @@ import { createPortal } from 'preact/compat';
interface Props {
content: ComponentChild;
overlayId?: string;
mask?: boolean;
onClose?: () => void;
onOk?: () => void;
onCancel?: () => void;
@ -59,8 +60,8 @@ export const Overlay = (props: Props) => {
isVisible ? (
<OverlayProvider value={controller}>
<Fragment>
<div class={styles.mask}></div>
<div class={styles.body}>{props.content}</div>
{props.mask ?? true ? <div class={styles.mask}></div> : null}
<div className={styles.body}>{props.content}</div>
</Fragment>
</OverlayProvider>
) : (

View File

@ -0,0 +1,10 @@
import { gql } from '@apollo/client';
export const LIST_PIPELINES = gql`
query ListPipelines($projectId: String) {
pipelines: listPipelines(projectId: $projectId) {
id
name
}
}
`;

View File

@ -0,0 +1,22 @@
.pipelineList {
@apply bg-red-200;
& > header {
@apply bg-white flex justify-between items-center;
}
}
.item {
@apply bg-gray-50;
}
.addBtn {
@apply bg-red-400 text-white;
@apply py-1 px-2 m-2 rounded-lg;
@apply hover:bg-red-500;
}
.refetchBtn {
@apply text-red-400 flex items-center;
@apply w-4 h-4 m-2 rounded-full;
}

View File

@ -0,0 +1,13 @@
// This file is automatically generated from your CSS. Any edits will be overwritten.
declare namespace PipelineListScssNamespace {
export interface IPipelineListScss {
addBtn: string;
item: string;
pipelineList: string;
refetchBtn: string;
}
}
declare const PipelineListScssModule: PipelineListScssNamespace.IPipelineListScss;
export = PipelineListScssModule;

View File

@ -0,0 +1,87 @@
import { useQuery } from '@apollo/client';
import { makeAutoObservable } from 'mobx';
import { useLocalObservable, useObserver } from 'mobx-react-lite';
import { useMemo } from 'preact/hooks';
import { h } from 'preact';
import { Pipeline } from '../../generated/graphql';
import styles from './pipeline-list.scss';
import classNames from 'classnames';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faRedoAlt } from '@fortawesome/free-solid-svg-icons';
import { createOverlay } from '../commons/overlay/overlay';
import { PipelineEditor } from '../../routes/pipelines/pipeline-editor';
import { LIST_PIPELINES } from './pipeline-list.constants';
interface Props {
projectId: string;
}
class Store {
pipelines?: Pipeline[];
currPipelineId?: string;
constructor() {
makeAutoObservable(this);
}
get items() {
return this.pipelines?.map(pipeline => {
const isActive = this.currPipelineId === pipeline.id;
return (
<li
key={pipeline.id}
className={classNames(
{
isActive
},
styles.item
)}
>
<h3>{pipeline.name}</h3>
</li>
);
});
}
setPipelines(pipelines: any[] | undefined) {
this.pipelines = pipelines;
}
}
export const PipelineList = ({ projectId }: Props) => {
const { data, refetch } = useQuery<{ pipelines: Pipeline[] }>(
LIST_PIPELINES,
{
variables: {
projectId
}
}
);
const pipelines: Pipeline[] | undefined = data?.pipelines;
const store = useLocalObservable(() => new Store());
useMemo(() => store.setPipelines(pipelines), [store, pipelines]);
const items = useObserver(() => store.items);
const addPipeline = () => {
createOverlay({
content: <PipelineEditor projectId={projectId} />
});
};
return (
<section className={styles.pipelineList}>
<header>
<button className={styles.addBtn} onClick={addPipeline}>
Add
</button>
<button className={styles.refetchBtn} onClick={() => refetch()}>
<FontAwesomeIcon icon={faRedoAlt} />
</button>
</header>
<ul>{items}</ul>
</section>
);
};

View File

@ -1,65 +0,0 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEdit, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
import { h } from 'preact';
import { Project } from '../../generated/graphql';
import styles from './project-details.scss';
import { createOverlay } from '../commons/overlay/overlay';
import { ProjectEditor } from './project-editor';
import { CommitLogList } from '../commit-logs/commit-log-list';
import { BranchesList } from '../branches-list/branches-list';
import { makeAutoObservable } from 'mobx';
import { Observer, useLocalObservable } from 'mobx-react';
interface Props {
project: Project;
}
class Store {
setBranch(branch?: string) {
this.branch = branch;
}
constructor() {
makeAutoObservable(this);
}
branch?: string;
}
export const ProjectDetails = ({ project }: Props) => {
const store = useLocalObservable(() => new Store());
const editProject = () => {
createOverlay({
content: <ProjectEditor project={project} />
});
};
const onSelectBranch = (branch?: string) => {
store.setBranch(branch);
};
return (
<section className={styles.projectDetails}>
<header>
<h2>
{project.name}
{project.webUrl ? (
<a target="blank" href={project.webUrl}>
<FontAwesomeIcon icon={faExternalLinkAlt} />
</a>
) : null}
</h2>
<small>{project.comment}</small>
<div className={styles.operations}>
<button onClick={editProject}>
<FontAwesomeIcon icon={faEdit} />
</button>
</div>
</header>
<div className={styles.body}>
<BranchesList project={project} onSelect={onSelectBranch} />
<Observer>
{(): any => <CommitLogList project={project} branch={store.branch} />}
</Observer>
</div>
</section>
);
};

View File

@ -1,34 +1 @@
.editor {
@apply bg-white shadow-lg p-4 rounded-lg text-gray-800
top-1/4 left-1/2 absolute
transform -translate-x-1/2 -translate-y-1/2
w-5/6;
}
.form {
@apply bg-white;
& > * {
@apply block;
}
label {
@apply my-4 relative flex;
}
.label {
@apply text-gray-700 w-20 inline-block text-right;
&::after {
content: '';
}
}
.controller {
@apply border-b border-gray-300 flex-auto;
}
.submitBtn {
@apply px-2 py-1 rounded-full bg-red-400 text-white;
}
.cancelBtn {
@apply px-2 py-1 rounded-full bg-gray-100 text-gray-700 ml-2;
}
.footer {
@apply text-right;
}
}
@import '../../style//editor.scss';

View File

@ -1,13 +1,20 @@
import { gql, useQuery } from '@apollo/client';
import { action, computed, makeObservable, observable, reaction } from 'mobx';
import {
action,
autorun,
computed,
makeObservable,
observable,
reaction
} from 'mobx';
import { useLocalObservable, useObserver } from 'mobx-react';
import { h } from 'preact';
import { route } from 'preact-router';
import { forwardRef } from 'preact/compat';
import { useImperativeHandle, useMemo, useRef } from 'preact/hooks';
import { appStore } from '../../app.store';
import { Project } from '../../generated/graphql';
import { createOverlay } from '../commons/overlay/overlay';
import { ProjectDetails } from './project-details';
import { ProjectEditor } from './project-editor';
import styles from './project-panel.scss';
@ -33,29 +40,32 @@ class Store {
@observable projects?: Project[];
constructor() {
makeObservable(this);
reaction(
() => this.details,
() => {
if (this.currentProject) {
appStore.setMain(this.details);
}
}
);
}
@computed
get currentProject() {
return this.projects?.find(it => it.id === this.currentProjectId);
}
@computed
get details() {
return this.currentProject ? (
<ProjectDetails project={this.currentProject} />
) : null;
get list() {
return this.projects?.map(item => (
<li
class={`${styles.item} ${
item.id === this.currentProject?.id ? styles.itemActive : ''
}`}
key={item.id}
onClick={() => this.setCurrentProjectId(item.id)}
>
<h3>{item.name}</h3>
<small>{item.comment}</small>
</li>
));
}
@action
setCurrentProjectId = (id: string) => {
this.currentProjectId = id;
route(`/projects/${id}`);
};
@action
@ -94,28 +104,15 @@ const List = forwardRef<ListRef>((_, ref) => {
const { data, refetch } = useQuery<{
projects: Project[];
}>(FIND_PROJECTS);
const projects = data?.projects;
const store = useLocalObservable(() => new Store());
useMemo(() => {
store.setProjects(data?.projects);
}, [data?.projects]);
store.setProjects(projects);
}, [projects, store]);
useImperativeHandle(ref, () => ({
refetch
}));
const list = store.projects?.map(item => (
<li
class={`${styles.item} ${
item.id === store.currentProject?.id ? styles.itemActive : ''
}`}
key={item.id}
onClick={() => store.setCurrentProjectId(item.id)}
>
<h3>{item.name}</h3>
<small>{item.comment}</small>
</li>
));
return useObserver(() => <ol class={styles.list}>{list}</ol>);
return useObserver(() => <ol class={styles.list}>{store.list}</ol>);
});

View File

@ -65,6 +65,47 @@ export type BranchList = {
all: Array<Scalars['String']>;
};
export type WorkUnit = {
__typename?: 'WorkUnit';
type: PipelineUnits;
scripts: Array<Scalars['String']>;
};
/** 流水线单元 */
export enum PipelineUnits {
Checkout = 'checkout',
InstallDependencies = 'installDependencies',
Test = 'test',
Deploy = 'deploy',
CleanUp = 'cleanUp'
}
export type WorkUnitMetadata = {
__typename?: 'WorkUnitMetadata';
version: Scalars['Float'];
units: Array<WorkUnit>;
};
export type Pipeline = {
__typename?: 'Pipeline';
id: Scalars['ID'];
project: Project;
projectId: Scalars['String'];
branch: Scalars['String'];
name: Scalars['String'];
workUnitMetadata: WorkUnitMetadata;
};
export type WorkUnitInput = {
type: PipelineUnits;
scripts: Array<Scalars['String']>;
};
export type WorkUnitMetadataInput = {
version?: Maybe<Scalars['Float']>;
units: Array<WorkUnitInput>;
};
export type Query = {
__typename?: 'Query';
hello: Hello;
@ -72,6 +113,8 @@ export type Query = {
findProject: Project;
listLogs: LogList;
listBranches: BranchList;
listPipelines: Array<Pipeline>;
findPipeline: Pipeline;
};
@ -89,6 +132,16 @@ export type QueryListBranchesArgs = {
listBranchesArgs: ListBranchesArgs;
};
export type QueryListPipelinesArgs = {
projectId?: Maybe<Scalars['String']>;
};
export type QueryFindPipelineArgs = {
id: Scalars['String'];
};
export type ListLogsArgs = {
projectId: Scalars['String'];
branch?: Maybe<Scalars['String']>;
@ -103,7 +156,9 @@ export type Mutation = {
createProject: Project;
modifyProject: Project;
deleteProject: Scalars['Float'];
checkout: Scalars['Boolean'];
createPipeline: Pipeline;
modifyPipeline: Pipeline;
deletePipeline: Scalars['Float'];
};
@ -123,8 +178,19 @@ export type MutationDeleteProjectArgs = {
};
export type MutationCheckoutArgs = {
checkoutInput: CheckoutInput;
export type MutationCreatePipelineArgs = {
pipeline: CreatePipelineInput;
};
export type MutationModifyPipelineArgs = {
Pipeline: UpdatePipelineInput;
id: Scalars['String'];
};
export type MutationDeletePipelineArgs = {
id: Scalars['String'];
};
export type CreateProjectInput = {
@ -143,8 +209,16 @@ export type UpdateProjectInput = {
webHookSecret?: Maybe<Scalars['String']>;
};
export type CheckoutInput = {
export type CreatePipelineInput = {
projectId: Scalars['String'];
branch?: Maybe<Scalars['String']>;
commitNumber?: Maybe<Scalars['String']>;
branch: Scalars['String'];
name: Scalars['String'];
workUnitMetadata: WorkUnitMetadataInput;
};
export type UpdatePipelineInput = {
projectId: Scalars['String'];
branch: Scalars['String'];
name: Scalars['String'];
workUnitMetadata: WorkUnitMetadataInput;
};

View File

@ -0,0 +1,6 @@
@import '../../style//editor.scss';
.workUnitMetadata {
min-height: 16rem;
@apply max-h-64;
}

View File

@ -0,0 +1,17 @@
// This file is automatically generated from your CSS. Any edits will be overwritten.
declare namespace PipelineEditorScssNamespace {
export interface IPipelineEditorScss {
cancelBtn: string;
controller: string;
editor: string;
footer: string;
form: string;
label: string;
submitBtn: string;
workUnitMetadata: string;
}
}
declare const PipelineEditorScssModule: PipelineEditorScssNamespace.IPipelineEditorScss;
export = PipelineEditorScssModule;

View File

@ -0,0 +1,213 @@
import { Field, Form, Formik } from 'formik';
import { h } from 'preact';
import { RoutableProps } from 'preact-router';
import styles from './pipeline-editor.scss';
import {
CreatePipelineInput,
Pipeline,
UpdatePipelineInput
} from '../../generated/graphql';
import { useOverlay } from '../../components/commons/overlay/overlay';
import { gql, useLazyQuery, useMutation } from '@apollo/client';
import { useMemo } from 'preact/hooks';
import classNames from 'classnames';
import { WorkUnitMetadata, PipelineUnits } from '../../generated/graphql';
import { Message } from '../../components/commons/message/index';
import { LIST_PIPELINES } from '../../components/pipelines/pipeline-list.constants';
const defaultWorkUnitMetadata: WorkUnitMetadata = {
version: 1,
units: [
{
type: PipelineUnits.Checkout,
scripts: []
},
{
type: PipelineUnits.InstallDependencies,
scripts: ['npm ci']
},
{
type: PipelineUnits.Test,
scripts: ['npm run test']
},
{
type: PipelineUnits.Deploy,
scripts: ['npm run build']
}
]
};
const FIND_PIPELINE = gql`
query FindPipeline($id: String!) {
pipeline: findPipeline(id: $id) {
name
id
projectId
workUnitMetadata {
version
units {
type
scripts
}
}
}
}
`;
const CREATE_PIPELINE = gql`
mutation CreatePipeline($input: CreatePipelineInput!) {
pipeline: createPipeline(pipeline: $input) {
id
projectId
name
workUnitMetadata {
version
units {
type
scripts
}
}
}
}
`;
const MODIFY_PIPELINE = gql`
mutation ModifyPipeline($id: String!, $input: UpdatePipelineInput!) {
pipeline: modifyPipeline(Pipeline: $input, id: $id) {
id
projectId
name
workUnitMetadata {
version
units {
type
scripts
}
}
}
}
`;
interface Props extends RoutableProps {
projectId: string;
id?: string;
}
export const PipelineEditor = ({ projectId, id }: Props) => {
type FormValues = Partial<CreatePipelineInput | UpdatePipelineInput> & {
workUnitMetadata: string;
};
const isCreate = !id;
const [loadPipeline, { data: pipeline }] = useLazyQuery(FIND_PIPELINE);
useMemo(() => {
if (!isCreate) {
loadPipeline();
}
}, []);
const formData: FormValues = pipeline ?? {
name: 'test',
projectId,
workUnitMetadata: JSON.stringify(defaultWorkUnitMetadata, null, 2)
};
const [createPipeline] = useMutation<{ pipeline: Pipeline }>(
CREATE_PIPELINE,
{
update(cache, { data }) {
if (!data) {
return;
}
const pipeline = data.pipeline;
cache.writeQuery({
query: LIST_PIPELINES,
variables: { projectId: pipeline.projectId },
data: {
pipelines: [
pipeline,
cache.readQuery({
query: LIST_PIPELINES,
variables: { projectId: pipeline.projectId }
})
]
}
});
}
}
);
const [modifyPipeline] = useMutation<Pipeline>(MODIFY_PIPELINE);
const { close } = useOverlay();
const cancel = (ev: MouseEvent) => {
ev.preventDefault();
close();
};
const submitForm = async (values: FormValues) => {
let workUnitMetadata: WorkUnitMetadata;
try {
workUnitMetadata = JSON.parse(values.workUnitMetadata);
} catch (err) {
Message.error('流程描述无法解析为JSON');
throw err;
}
values = Object.assign({}, values, { workUnitMetadata });
try {
if (isCreate) {
await createPipeline({
variables: { input: values }
});
} else {
await modifyPipeline({
variables: { input: values, id }
});
}
close();
} finally {
//
}
};
return (
<section className={styles.editor}>
<Formik initialValues={formData} onSubmit={submitForm}>
<Form className={styles.form}>
<label>
<span className={styles.label}></span>
<Field
className={styles.controller}
name="name"
placeholder="流水线名称"
></Field>
</label>
<label>
<span className={styles.label}>Branch</span>
<Field
className={styles.controller}
name="branch"
placeholder="Branch"
></Field>
</label>
<label>
<span className={styles.label}></span>
<Field
className={classNames(styles.controller, styles.workUnitMetadata)}
name="workUnitMetadata"
placeholder="流程描述JSON"
as="textarea"
></Field>
</label>
<footer className={styles.footer}>
<button type="submit" className={styles.submitBtn}>
{isCreate ? '创建' : '修改'}
</button>
<button onClick={cancel} className={styles.cancelBtn}>
</button>
</footer>
</Form>
</Formik>
</section>
);
};

View File

@ -1,6 +1,6 @@
.projectDetails {
@apply h-full flex flex-col;
header {
& > header {
@apply bg-red-400 text-gray-50 p-2;
@apply grid grid-cols-2 grid-rows-2;

View File

@ -0,0 +1,96 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEdit, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
import { h } from 'preact';
import { Project } from '../../generated/graphql';
import styles from './project-details.scss';
import { createOverlay } from '../../components/commons/overlay/overlay';
import { ProjectEditor } from '../../components/projects/project-editor';
import { CommitLogList } from '../../components/commit-logs/commit-log-list';
import { makeAutoObservable } from 'mobx';
import { Observer, useLocalObservable } from 'mobx-react';
import { RoutableProps } from 'preact-router';
import { gql, useQuery } from '@apollo/client';
import { PipelineList } from '../../components/pipelines/pipeline-list';
const FIND_PROJECT = gql`
query FindProject($id: String!) {
project: findProject(id: $id) {
id
name
comment
sshUrl
webUrl
webHookSecret
deletedAt
}
}
`;
interface Props extends RoutableProps {
id: string;
}
class Store {
setBranch(branch?: string) {
this.branch = branch;
}
constructor() {
makeAutoObservable(this);
}
branch?: string;
}
export const ProjectDetails = ({ id, path }: Props) => {
const { data } = useQuery<{ project: Project }, { id: string }>(
FIND_PROJECT,
{
variables: {
id
}
}
);
const project: Project | undefined = data?.project;
const store = useLocalObservable(() => new Store());
const editProject = () => {
createOverlay({
content: <ProjectEditor project={project} />
});
};
const onSelectBranch = (branch?: string) => {
store.setBranch(branch);
};
return (
<section className={styles.projectDetails}>
<header>
<h2>
{project?.name}
{project?.webUrl ? (
<a target="blank" href={project?.webUrl}>
<FontAwesomeIcon icon={faExternalLinkAlt} />
</a>
) : null}
</h2>
<small>{project?.comment}</small>
<div className={styles.operations}>
<button onClick={editProject}>
<FontAwesomeIcon icon={faEdit} />
</button>
</div>
</header>
{project ? (
<div className={styles.body}>
<PipelineList projectId={id} />
<Observer>
{(): any => (
<CommitLogList project={project} branch={store.branch} />
)}
</Observer>
</div>
) : null}
</section>
);
};

34
src/style/editor.scss Normal file
View File

@ -0,0 +1,34 @@
.editor {
@apply bg-white shadow-lg p-4 rounded-lg text-gray-800
top-1/4 left-1/2 absolute
transform -translate-x-1/2 -translate-y-1/2
w-5/6;
}
.form {
@apply bg-white;
& > * {
@apply block;
}
label {
@apply my-4 relative flex;
}
.label {
@apply text-gray-700 w-20 inline-block text-right;
&::after {
content: '';
}
}
.controller {
@apply border-b border-gray-300 flex-auto;
}
.submitBtn {
@apply px-2 py-1 rounded-full bg-red-400 text-white;
}
.cancelBtn {
@apply px-2 py-1 rounded-full bg-gray-100 text-gray-700 ml-2;
}
.footer {
@apply text-right;
}
}

View File

@ -0,0 +1,26 @@
import { Message } from './../components/commons/message/index';
import { ApolloClient, concat, HttpLink, InMemoryCache } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
export function createApolloClient() {
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
console.error(graphQLErrors);
Message.error(graphQLErrors.map(error => error.message).join());
graphQLErrors.forEach(({ message, locations, path }) => {
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
);
});
}
if (networkError) console.log(`[Network error]: ${networkError}`);
});
return new ApolloClient({
cache: new InMemoryCache(),
link: concat(
errorLink,
new HttpLink({
uri: '/api/graphql'
})
)
});
}