Compare commits

...

4 Commits

Author SHA1 Message Date
Ivan Li
1170e46477 feat(pipeline-tasks): 任务详情页面效果优化。 2021-04-05 16:33:22 +08:00
Ivan Li
2ff7169e73 feat(commit-logs): 改善列表样式。 2021-04-05 10:23:27 +08:00
Ivan Li
283a5a82a5 refactor: echo route. 2021-04-05 10:00:33 +08:00
Ivan Li
f34bab0819 refactor: move components from src/routes/ to /src/components 2021-04-04 16:07:43 +08:00
20 changed files with 235 additions and 280 deletions

View File

@ -2,6 +2,8 @@
"css.validate": false, "css.validate": false,
"scss.validate": false, "scss.validate": false,
"cSpell.words": [ "cSpell.words": [
"Formik" "Formik",
"autorun",
"wouter"
] ]
} }

10
package-lock.json generated
View File

@ -15492,11 +15492,6 @@
"pretty-format": "^3.8.0" "pretty-format": "^3.8.0"
} }
}, },
"preact-router": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/preact-router/-/preact-router-3.2.1.tgz",
"integrity": "sha512-KEN2VN1DxUlTwzW5IFkF13YIA2OdQ2OvgJTkQREF+AA2NrHRLaGbB68EjS4IeZOa1shvQ1FvEm3bSLta4sXBhg=="
},
"prelude-ls": { "prelude-ls": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
@ -21144,6 +21139,11 @@
"microevent.ts": "~0.1.1" "microevent.ts": "~0.1.1"
} }
}, },
"wouter-preact": {
"version": "2.7.4",
"resolved": "https://registry.npmjs.org/wouter-preact/-/wouter-preact-2.7.4.tgz",
"integrity": "sha512-lYYO3JozPcHB9VP3FrjliADVD04p0TDFRP28wVFWfIGN8l8RxCTWXQxvm9uQ0vFU7CBUA2nOFeCveBPXdZiGLQ=="
},
"wrap-ansi": { "wrap-ansi": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",

View File

@ -41,10 +41,10 @@
"preact-jsx-chai": "^3.0.0", "preact-jsx-chai": "^3.0.0",
"preact-markup": "^2.1.1", "preact-markup": "^2.1.1",
"preact-render-to-string": "^5.1.18", "preact-render-to-string": "^5.1.18",
"preact-router": "^3.2.1",
"ramda": "^0.27.1", "ramda": "^0.27.1",
"subscriptions-transport-ws": "^0.9.18", "subscriptions-transport-ws": "^0.9.18",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.3" "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.3",
"wouter-preact": "^2.7.4"
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/cli": "^1.21.3", "@graphql-codegen/cli": "^1.21.3",

View File

@ -4,10 +4,10 @@ import { ProjectPanel } from './projects/project-panel';
import styles from './app.scss'; import styles from './app.scss';
import { OverlayContainer } from './commons/overlay/overlay'; import { OverlayContainer } from './commons/overlay/overlay';
import { useObserver } from 'mobx-react'; import { useObserver } from 'mobx-react';
import Router, { Route } from 'preact-router'; import { Router, Route } from 'wouter-preact';
import { ProjectDetails } from '../routes/projects/project-details';
import { createApolloClient } from '../units/apollo-client'; import { createApolloClient } from '../units/apollo-client';
import { PipelineEditor } from '../routes/pipelines/pipeline-editor'; import { ProjectDetails } from './pipeline-tasks/project-details';
import { PipelineEditor } from './pipelines/pipeline-editor';
const client = createApolloClient(); const client = createApolloClient();
@ -25,13 +25,16 @@ const App: FunctionalComponent = () => {
const Content = () => { const Content = () => {
return <Board />; return <Board />;
}; };
const Board = () => { const Board = () => {
return useObserver(() => { return useObserver(() => {
return ( return (
<Fragment> <Fragment>
<main> <main>
<Router> <Router>
<Route path="/projects/:id/:rest*" component={ProjectDetails} /> <Route path="/projects/:projectId/:rest*">
{params => <ProjectDetails projectId={params.projectId} />}
</Route>
<Route <Route
path="/dev" path="/dev"
component={() => <PipelineEditor projectId="test" />} component={() => <PipelineEditor projectId="test" />}

View File

@ -21,7 +21,7 @@ import {
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { useLocalObservable } from 'mobx-react'; import { useLocalObservable } from 'mobx-react';
import { PipelineTask, PipelineUnits } from '../../generated/graphql'; import { PipelineTask, PipelineUnits } from '../../generated/graphql';
import { route } from 'preact-router'; import { useLocation } from 'wouter-preact';
const CREATE_PIPELINE_TASK = gql` const CREATE_PIPELINE_TASK = gql`
mutation CreatePipelineTask($task: CreatePipelineTaskInput!) { mutation CreatePipelineTask($task: CreatePipelineTaskInput!) {
@ -42,18 +42,12 @@ interface Props {
branch?: string; branch?: string;
} }
class Store {
constructor() {
makeAutoObservable(this);
}
isTasksWorking = [false, false, false, false];
}
export const CommitActions = ({ pipeline, commit }: Props) => { export const CommitActions = ({ pipeline, commit }: Props) => {
const [createTask] = useMutation< const [createTask] = useMutation<
{ task: PipelineTask }, { task: PipelineTask },
{ task: CreatePipelineTaskInput } { task: CreatePipelineTaskInput }
>(CREATE_PIPELINE_TASK); >(CREATE_PIPELINE_TASK);
const setLocation = useLocation()[1];
const doWork = async (units: PipelineUnits[]) => { const doWork = async (units: PipelineUnits[]) => {
const { data } = await createTask({ const { data } = await createTask({
@ -61,9 +55,7 @@ export const CommitActions = ({ pipeline, commit }: Props) => {
task: { pipelineId: pipeline.id, commit, units } task: { pipelineId: pipeline.id, commit, units }
} }
}); });
route( setLocation(`/tasks/${data?.task.id}`);
`/projects/${pipeline.projectId}/pipelines/${pipeline.id}/tasks/${data?.task.id}`
);
}; };
const commitActionsStore = useLocalObservable(() => new CommitActionsStore()); const commitActionsStore = useLocalObservable(() => new CommitActionsStore());

View File

@ -21,3 +21,11 @@
@apply justify-self-end self-center; @apply justify-self-end self-center;
} }
} }
.taskList {
@apply text-sm bg-gray-100;
li {
@apply px-2 py-1 cursor-pointer;
@apply hover:bg-gray-200;
}
}

View File

@ -4,6 +4,7 @@ declare namespace CommitLogListScssNamespace {
component: string; component: string;
item: string; item: string;
list: string; list: string;
taskList: string;
} }
} }

View File

@ -4,8 +4,8 @@ import { gql, useQuery, useSubscription } from '@apollo/client';
import styles from './commit-log-list.scss'; import styles from './commit-log-list.scss';
import { CommitActions } from '../commit-actions/commit-actions'; import { CommitActions } from '../commit-actions/commit-actions';
import { FIND_PIPELINE } from '../pipelines/pipeline-list.constants'; import { FIND_PIPELINE } from '../pipelines/pipeline-list.constants';
import { route } from 'preact-router';
import { PipelineTaskStatus } from '../pipeline-tasks/pipeline-task-status'; import { PipelineTaskStatus } from '../pipeline-tasks/pipeline-task-status';
import { useLocation } from 'wouter-preact';
const LIST_LOGS = gql` const LIST_LOGS = gql`
subscription listLogsForPipeline($id: String!) { subscription listLogsForPipeline($id: String!) {
@ -41,6 +41,7 @@ export const CommitLogList = ({ pipelineId }: Props) => {
id: pipelineId! id: pipelineId!
} }
}); });
const setLocation = useLocation()[1];
const queryResult = useQuery<{ pipeline: Pipeline }>(FIND_PIPELINE, { const queryResult = useQuery<{ pipeline: Pipeline }>(FIND_PIPELINE, {
variables: { id: pipelineId } variables: { id: pipelineId }
}); });
@ -54,16 +55,9 @@ export const CommitLogList = ({ pipelineId }: Props) => {
<CommitActions pipeline={pipeline} commit={log.hash} /> <CommitActions pipeline={pipeline} commit={log.hash} />
) : null} ) : null}
</header> </header>
<ol> <ol className={styles.taskList}>
{log.tasks.map(task => ( {log.tasks.map(task => (
<li <li key={task.id} onClick={() => setLocation(`/tasks/${task.id}`)}>
key={task.id}
onClick={() =>
route(
`/projects/${pipeline?.projectId}/pipelines/${pipeline?.id}/tasks/${task.id}`
)
}
>
<PipelineTaskStatus status={task.status} /> {task.startedAt} <PipelineTaskStatus status={task.status} /> {task.startedAt}
</li> </li>
))} ))}

View File

@ -11,6 +11,9 @@
&:not(:first-child) { &:not(:first-child) {
@apply border-l border-gray-200; @apply border-l border-gray-200;
} }
& > svg {
@apply ml-2 text-sm;
}
} }
.LogListOfUnit { .LogListOfUnit {
@apply overflow-scroll; @apply overflow-scroll;
@ -19,7 +22,7 @@
@apply bg-gray-200; @apply bg-gray-200;
} }
.unitLogs { .unitLogs {
@apply whitespace-pre-line font-mono text-sm; @apply whitespace-pre-line font-mono text-xs;
@apply p-1 my-px; @apply p-1 my-px;
@apply bg-gray-100; @apply bg-gray-100;
} }

View File

@ -1,7 +1,7 @@
import { gql, useSubscription, useQuery } from '@apollo/client'; import { gql, useSubscription, useQuery } from '@apollo/client';
import { h } from 'preact'; import { h } from 'preact';
import { useMemo } from 'preact/hooks'; import { useMemo } from 'preact/hooks';
import { find, last, propEq } from 'ramda'; import { find, findIndex, last, propEq } from 'ramda';
import { observer, useLocalObservable, useObserver } from 'mobx-react'; import { observer, useLocalObservable, useObserver } from 'mobx-react';
import { autorun, makeAutoObservable } from 'mobx'; import { autorun, makeAutoObservable } from 'mobx';
import styles from './pipeline-task-details.scss'; import styles from './pipeline-task-details.scss';
@ -12,13 +12,7 @@ import {
PipelineTask, PipelineTask,
PipelineTaskLogMessage PipelineTaskLogMessage
} from '../../generated/graphql'; } from '../../generated/graphql';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { PipelineTaskStatus } from './pipeline-task-status';
import {
faExclamationCircle,
faPauseCircle,
faSpinner
} from '@fortawesome/free-solid-svg-icons';
import classNames from 'classnames';
interface Props { interface Props {
taskId: string; taskId: string;
} }
@ -44,12 +38,26 @@ class Store {
} }
addLogsFromTask(task: PipelineTask) { addLogsFromTask(task: PipelineTask) {
if (task.status !== TaskStatuses.Pending) {
const unit = (task.logs[0]?.unit as unknown) as PipelineUnits;
if (unit) {
const log = find(propEq('unit', unit), this.logs);
if (!log) {
this.logs.push({
unit: unit,
status: TaskStatuses.Working,
startedAt: task.startedAt,
logs: ''
});
}
}
}
for (const log of task.logs) { for (const log of task.logs) {
const taskLog = find(propEq('unit', log.unit), this.logs); const index = findIndex(propEq('unit', log.unit), this.logs);
if (!taskLog) { if (index === -1) {
this.logs.push(log); this.logs.push(log);
} else { } else {
taskLog.logs = log.logs + taskLog.logs; this.logs[index] = log;
} }
} }
} }
@ -66,7 +74,6 @@ class Store {
taskLog.logs += log.message; taskLog.logs += log.message;
} }
} }
setTask(task: PipelineTask) { setTask(task: PipelineTask) {
this.task = task; this.task = task;
} }
@ -84,13 +91,16 @@ const FIND_PIPELINE_TASK = gql`
logs { logs {
unit unit
logs logs
status
startedAt
endedAt
} }
} }
} }
`; `;
const PIPELINE_TASK_CHANGED = gql` const PIPELINE_TASK_CHANGED = gql`
subscription Pipelinetaskchanged($taskId: String!) { subscription PipelineTaskChanged($taskId: String!) {
task: pipelineTaskChanged(id: $taskId) { task: pipelineTaskChanged(id: $taskId) {
id id
units units
@ -98,6 +108,13 @@ const PIPELINE_TASK_CHANGED = gql`
status status
startedAt startedAt
endedAt endedAt
logs {
unit
status
logs
startedAt
endedAt
}
} }
} }
`; `;
@ -167,11 +184,15 @@ const UnitList = observer(({ store }: { store: Store }) => {
return <div>Loading...</div>; return <div>Loading...</div>;
} }
const tabs = store.task.units.map(unit => ( const tabs = store.task.units.map(unit => {
<li key={unit} className={styles.navItem}> const log = find(propEq('unit', unit), store.logs);
{unit} return (
</li> <li key={unit} className={styles.navItem}>
)); <span>{unit}</span>
<PipelineTaskStatus status={log?.status || TaskStatuses.Pending} />
</li>
);
});
return <ol className={styles.navContainer}>{tabs}</ol>; return <ol className={styles.navContainer}>{tabs}</ol>;
}); });
@ -197,48 +218,9 @@ const TaskInfo = observer(({ store }: { store: Store }) => {
} }
return ( return (
<header className={styles.taskInfo}> <header className={styles.taskInfo}>
<StatusIcon status={task.status} /> <PipelineTaskStatus status={task.status} />
<p>{task.startedAt}</p> <p>{task.startedAt}</p>
<p>{task.status}</p> <p>{task.status}</p>
</header> </header>
); );
}); });
const StatusIcon = ({ status }: { status: TaskStatuses }) => {
switch (status) {
case TaskStatuses.Working:
return (
<FontAwesomeIcon
icon={faSpinner}
className={classNames([
'animate-spin',
styles.taskStatusIcon,
'text-purple-500'
])}
/>
);
case TaskStatuses.Failed:
return (
<FontAwesomeIcon
icon={faExclamationCircle}
className={classNames([styles.taskStatusIcon, 'text-red-700'])}
/>
);
case TaskStatuses.Success:
return (
<FontAwesomeIcon
icon={faExclamationCircle}
className={classNames([styles.taskStatusIcon, 'text-green-500'])}
/>
);
case TaskStatuses.Pending:
return (
<FontAwesomeIcon
icon={faPauseCircle}
className={classNames([styles.taskStatusIcon, 'text-yellow-500'])}
/>
);
default:
return null;
}
};

View File

@ -0,0 +1,82 @@
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 { Router, Route } from 'wouter-preact';
import { gql, useQuery } from '@apollo/client';
import { PipelineList } from '../../components/pipelines/pipeline-list';
import { PipelineTaskDetails } from '../../components/pipeline-tasks/pipeline-task-details';
const FIND_PROJECT = gql`
query FindProject($id: String!) {
project: findProject(id: $id) {
id
name
comment
sshUrl
webUrl
webHookSecret
}
}
`;
interface Props {
projectId: string;
}
export const ProjectDetails = ({ projectId: id }: Props) => {
const { data } = useQuery<{ project: Project }, { id: string }>(
FIND_PROJECT,
{
variables: {
id
}
}
);
const project: Project | undefined = data?.project;
const editProject = () => {
createOverlay({
content: <ProjectEditor project={project} />
});
};
return (
<Router base={`/projects/${id}`} key={id}>
<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} />
<Route path="/tasks/:taskId">
{({ taskId }) => <PipelineTaskDetails taskId={taskId} />}
</Route>
<Route path="/pipelines/:pipelineId">
{({ pipelineId }) => <CommitLogList pipelineId={pipelineId} />}
</Route>
</div>
) : null}
</section>
</Router>
);
};

View File

@ -3,4 +3,4 @@
.workUnitMetadata { .workUnitMetadata {
min-height: 16rem; min-height: 16rem;
@apply max-h-64; @apply max-h-64;
} }

View File

@ -1,6 +1,5 @@
import { Field, Form, Formik, useFormik } from 'formik'; import { Field, Form, Formik } from 'formik';
import { h } from 'preact'; import { h } from 'preact';
import { RoutableProps } from 'preact-router';
import styles from './pipeline-editor.scss'; import styles from './pipeline-editor.scss';
import { useOverlay } from '../../components/commons/overlay/overlay'; import { useOverlay } from '../../components/commons/overlay/overlay';
import { gql, useLazyQuery, useMutation } from '@apollo/client'; import { gql, useLazyQuery, useMutation } from '@apollo/client';
@ -75,7 +74,7 @@ const MODIFY_PIPELINE = gql`
} }
`; `;
interface Props extends RoutableProps { interface Props {
projectId: string; projectId: string;
id?: string; id?: string;
} }

View File

@ -1,7 +1,7 @@
import { gql, useQuery, useMutation } from '@apollo/client'; import { gql, useQuery, useMutation } from '@apollo/client';
import { autorun, makeAutoObservable, reaction } from 'mobx'; import { makeAutoObservable } from 'mobx';
import { useLocalObservable } from 'mobx-react-lite'; import { useLocalObservable } from 'mobx-react-lite';
import { useCallback, useMemo } from 'preact/hooks'; import { useMemo, useState } from 'preact/hooks';
import { h } from 'preact'; import { h } from 'preact';
import { Pipeline } from '../../generated/graphql'; import { Pipeline } from '../../generated/graphql';
import styles from './pipeline-list.scss'; import styles from './pipeline-list.scss';
@ -15,10 +15,10 @@ import {
faTrash faTrash
} from '@fortawesome/free-solid-svg-icons'; } from '@fortawesome/free-solid-svg-icons';
import { createOverlay } from '../commons/overlay/overlay'; import { createOverlay } from '../commons/overlay/overlay';
import { PipelineEditor } from '../../routes/pipelines/pipeline-editor'; import { PipelineEditor } from './pipeline-editor';
import { LIST_PIPELINES } from './pipeline-list.constants'; import { LIST_PIPELINES } from './pipeline-list.constants';
import { Observer, observer } from 'mobx-react'; import { Observer, observer } from 'mobx-react';
import { getCurrentUrl, route } from 'preact-router'; import { useLocation, useRoute } from 'wouter-preact';
const DELETE_PIPELINE = gql` const DELETE_PIPELINE = gql`
mutation DeletePipeline($id: String!) { mutation DeletePipeline($id: String!) {
@ -32,29 +32,25 @@ interface Props {
class Store { class Store {
pipelines?: Pipeline[]; pipelines?: Pipeline[];
currPipelineId?: string;
editMode = false; editMode = false;
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);
} }
refetching = false; refetching = false;
setPipelines(pipelines: any[] | undefined) { setPipelines(pipelines: Pipeline[] | undefined) {
this.pipelines = pipelines; this.pipelines = pipelines;
} }
setRefetching(val: boolean) { setRefetching(val: boolean) {
this.refetching = val; this.refetching = val;
} }
setCurrPipelineId(id: string) {
this.currPipelineId = id;
}
toggleEditMode() { toggleEditMode() {
this.editMode = !this.editMode; this.editMode = !this.editMode;
} }
} }
export const PipelineList = ({ projectId }: Props) => { export const PipelineList = ({ projectId }: Props) => {
const { data, refetch, loading } = useQuery<{ pipelines: Pipeline[] }>( const { data, refetch } = useQuery<{ pipelines: Pipeline[] }>(
LIST_PIPELINES, LIST_PIPELINES,
{ {
variables: { variables: {
@ -67,15 +63,26 @@ export const PipelineList = ({ projectId }: Props) => {
const store = useLocalObservable(() => new Store()); const store = useLocalObservable(() => new Store());
useMemo(() => store.setPipelines(pipelines), [store, pipelines]); const [currentPipeline, setCurrentPipeline] = useState<Pipeline | undefined>(
undefined
reaction(
() => store.currPipelineId,
() => {
route(`/projects/${projectId}/pipelines/${store.currPipelineId}`);
}
); );
useMemo(() => store.setPipelines(pipelines), [store, pipelines]);
const params = useRoute('/pipelines/:pipelineId')[1];
useMemo(() => {
let pipeline = pipelines?.find(
pipeline => pipeline.id === params?.pipelineId
);
if (!pipeline) {
pipeline = pipelines?.[0];
}
if (pipeline === currentPipeline) {
return;
}
setCurrentPipeline(pipeline);
}, [params, pipelines]);
const Header = observer(() => { const Header = observer(() => {
const addPipeline = () => { const addPipeline = () => {
createOverlay({ createOverlay({
@ -110,7 +117,13 @@ export const PipelineList = ({ projectId }: Props) => {
}); });
const items = pipelines?.map(pipeline => ( const items = pipelines?.map(pipeline => (
<Observer key={pipeline.id}> <Observer key={pipeline.id}>
{(): any => <Item pipeline={pipeline} store={store} />} {(): any => (
<Item
pipeline={pipeline}
store={store}
currentPipeline={currentPipeline}
/>
)}
</Observer> </Observer>
)); ));
@ -123,7 +136,16 @@ export const PipelineList = ({ projectId }: Props) => {
}; };
const Item = observer( const Item = observer(
({ pipeline, store }: { pipeline: Pipeline; store: Store }) => { ({
pipeline,
store,
currentPipeline
}: {
pipeline: Pipeline;
store: Store;
currentPipeline?: Pipeline;
}) => {
const setLocation = useLocation()[1];
const [remove] = useMutation(DELETE_PIPELINE, { const [remove] = useMutation(DELETE_PIPELINE, {
variables: { id: pipeline.id }, variables: { id: pipeline.id },
update(cache) { update(cache) {
@ -152,7 +174,7 @@ const Item = observer(
} }
}); });
const isActive = store.currPipelineId === pipeline.id; const isActive = currentPipeline?.id === pipeline.id;
const openModifyPanel = () => { const openModifyPanel = () => {
createOverlay({ createOverlay({
content: ( content: (
@ -184,7 +206,7 @@ const Item = observer(
}, },
styles.item styles.item
)} )}
onClick={() => store.setCurrPipelineId(pipeline.id)} onClick={() => setLocation(`/pipelines/${pipeline.id}`)}
> >
<h3>{pipeline.name}</h3> <h3>{pipeline.name}</h3>
<small> <small>

View File

@ -1,18 +1,8 @@
import { gql, useQuery } from '@apollo/client'; import { gql, useQuery } from '@apollo/client';
import {
action,
autorun,
computed,
makeObservable,
observable,
reaction
} from 'mobx';
import { useLocalObservable, useObserver } from 'mobx-react';
import { h } from 'preact'; import { h } from 'preact';
import { route } from 'preact-router'; import { forwardRef, useState } from 'preact/compat';
import { forwardRef } from 'preact/compat';
import { useImperativeHandle, useMemo, useRef } from 'preact/hooks'; import { useImperativeHandle, useMemo, useRef } from 'preact/hooks';
import { appStore } from '../../app.store'; import { useLocation, useRoute } from 'wouter-preact';
import { Project } from '../../generated/graphql'; import { Project } from '../../generated/graphql';
import { createOverlay } from '../commons/overlay/overlay'; import { createOverlay } from '../commons/overlay/overlay';
import { ProjectEditor } from './project-editor'; import { ProjectEditor } from './project-editor';
@ -33,45 +23,6 @@ const FIND_PROJECTS = gql`
interface ListRef { interface ListRef {
refetch: () => void; refetch: () => void;
} }
class Store {
@observable currentProjectId?: string;
@observable projects?: Project[];
constructor() {
makeObservable(this);
}
@computed
get currentProject() {
return this.projects?.find(it => it.id === this.currentProjectId);
}
@computed
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
setProjects = (projects?: Project[]) => {
this.projects = projects;
};
}
export function ProjectPanel() { export function ProjectPanel() {
const listRef = useRef<ListRef>(); const listRef = useRef<ListRef>();
const addProject = () => { const addProject = () => {
@ -104,14 +55,36 @@ const List = forwardRef<ListRef>((_, ref) => {
projects: Project[]; projects: Project[];
}>(FIND_PROJECTS); }>(FIND_PROJECTS);
const projects = data?.projects; const projects = data?.projects;
const setLocation = useLocation()[1];
const store = useLocalObservable(() => new Store()); const params = useRoute('/projects/:projectId/:rest*')[1];
const [currentProject, setCurrentProject] = useState<Project | undefined>(
undefined
);
useMemo(() => { useMemo(() => {
store.setProjects(projects); let project = projects?.find(project => params?.projectId === project.id);
}, [projects, store]); if (!project) {
project = projects?.[0];
}
if (project === currentProject) {
return;
}
setCurrentProject(project);
}, [params, projects]);
const items = projects?.map(item => (
<li
class={`${styles.item} ${
item.id === currentProject?.id ? styles.itemActive : ''
}`}
key={item.id}
onClick={() => setLocation(`/projects/${item.id}`)}
>
<h3>{item.name}</h3>
<small>{item.comment}</small>
</li>
));
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
refetch refetch
})); }));
return useObserver(() => <ol class={styles.list}>{store.list}</ol>); return <ol class={styles.list}>{items}</ol>;
}); });

View File

@ -1,106 +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 '../../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 Router, { RoutableProps, Route } from 'preact-router';
import { gql, useQuery } from '@apollo/client';
import { PipelineList } from '../../components/pipelines/pipeline-list';
import { PipelineTaskList } from '../../components/pipeline-tasks/pipeline-task-list';
import { PipelineTaskDetails } from '../../components/pipeline-tasks/pipeline-task-details';
const FIND_PROJECT = gql`
query FindProject($id: String!) {
project: findProject(id: $id) {
id
name
comment
sshUrl
webUrl
webHookSecret
}
}
`;
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 => (
<Router>
<Route
path="/projects/:projectId/pipelines/:pipelineId/tasks/:taskId"
component={PipelineTaskDetails}
/>
<Route
path="/projects/:projectId/pipelines/:pipelineId"
component={CommitLogList}
/>
</Router>
)}
</Observer>
</div>
) : null}
</section>
);
};

View File

@ -3,7 +3,7 @@
/* Basic Options */ /* Basic Options */
"target": "ES6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, "target": "ES6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */,
"module": "ESNext" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, "module": "ESNext" /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation: */ "lib": ["ESNext"], /* Specify library files to be included in the compilation: */
"allowJs": true /* Allow javascript files to be compiled. */, "allowJs": true /* Allow javascript files to be compiled. */,
// "checkJs": true, /* Report errors in .js files. */ // "checkJs": true, /* Report errors in .js files. */
"jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,