Compare commits

...

10 Commits

Author SHA1 Message Date
Ivan Li
f810aedfaa feat(projects): list, create and update. 2021-05-04 21:08:00 +08:00
Ivan Li
7d04d53d02 build: use pm2 serve for prod. 2021-05-03 12:03:18 +08:00
Ivan Li
e21e6fc3e2 build(dep): remove @types/date-fns. 2021-05-03 10:35:30 +08:00
Ivan Li
9334a45e55 build: pm2 ecosystem config. 2021-05-03 10:21:42 +08:00
Ivan Li
d29eeaae90 build: fix vscode launch.json. 2021-05-02 22:14:16 +08:00
Ivan Li
4a533748f3 build: 复原 graphql 配置,修改开发 port. 2021-05-01 17:45:31 +08:00
Ivan Li
183564aed8 feat: Article 模块 2021-05-01 15:13:38 +08:00
Ivan
0b5fa5b643 feat: route-react -> curi. 2021-04-28 13:41:09 +08:00
Ivan Li
2e70c31849 backup 2021-04-24 14:47:22 +08:00
Ivan Li
065e1679ba feat: INIT PROJECT. 2021-04-18 11:18:29 +08:00
20 changed files with 32577 additions and 72 deletions

17
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "chrome",
"type": "chrome",
"request": "launch",
"reAttach": true,
"url": "http://fennec.localhost/",
"webRoot": "${workspaceFolder}",
"userDataDir": "/Users/ivan/Projects/.chrome"
}
]
}

8
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,8 @@
{
"cSpell.words": [
"Formik",
"clsx",
"fontsource",
"vditor"
]
}

8
apollo.config.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
client: {
service: {
name: 'fennec-be',
url: 'http://api.fennec.localhost/graphql'
}
}
};

12
codegen.yml Normal file
View File

@ -0,0 +1,12 @@
overwrite: true
schema: "http://api.fennec.localhost/graphql"
# documents: "src/**/*.graphql"
generates:
src/generated/graphql.tsx:
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"
./graphql.schema.json:
plugins:
- "introspection"

19
ecosystem.config.js Normal file
View File

@ -0,0 +1,19 @@
module.exports = {
apps: [
{
name: "fennec-bs",
script: "serve",
args: "",
watch: false,
ignore_watch: ["node_modules"],
log_date_format: "MM-DD HH:mm:ss.SSS Z",
env: {
PM2_SERVE_PATH: "./build",
PM2_SERVE_PORT: 7135,
PM2_SERVE_SPA: "true",
PM2_SERVE_HOMEPAGE: "/index.html",
},
max_restarts: 5,
},
],
};

3103
graphql.schema.json Normal file

File diff suppressed because it is too large Load Diff

28555
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,17 @@
{ {
"name": "blog-bs", "name": "fennec-bs",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@apollo/client": "^3.3.15",
"@curi/react-dom": "^2.0.4",
"@curi/router": "^2.1.2",
"@date-io/date-fns": "^1.3.13",
"@hickory/browser": "^2.1.0",
"@material-ui/core": "^4.11.3",
"@material-ui/icons": "^4.11.2",
"@material-ui/lab": "*",
"@material-ui/pickers": "^3.3.10",
"@testing-library/jest-dom": "^5.11.10", "@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.6", "@testing-library/react": "^11.2.6",
"@testing-library/user-event": "^12.8.3", "@testing-library/user-event": "^12.8.3",
@ -10,17 +19,28 @@
"@types/node": "^12.20.10", "@types/node": "^12.20.10",
"@types/react": "^17.0.3", "@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3", "@types/react-dom": "^17.0.3",
"date-fns": "^2.21.1",
"fontsource-roboto": "^4.0.0",
"formik": "^2.2.6",
"formik-material-ui": "^3.0.1",
"formik-material-ui-pickers": "^0.0.12",
"graphql": "^15.5.0",
"ramda": "^0.27.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"typescript": "^4.2.4", "typescript": "^4.2.4",
"web-vitals": "^1.1.1" "vditor": "^3.8.4",
"web-vitals": "^1.1.1",
"yup": "^0.32.9"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "PORT=7123 BROWSER=none react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "eject": "react-scripts eject",
"prestart": "npm run graphql",
"graphql": "graphql-codegen --config codegen.yml"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@ -39,5 +59,22 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"@graphql-codegen/cli": "1.21.3",
"@graphql-codegen/introspection": "1.18.1",
"@graphql-codegen/typescript": "1.21.1",
"@graphql-codegen/typescript-operations": "1.17.15",
"@graphql-codegen/typescript-react-apollo": "2.2.3",
"@types/autoprefixer": "^10.2.0",
"@types/graphql": "^14.5.0",
"@types/ramda": "^0.27.40",
"@types/sass": "^1.16.0",
"@types/tailwindcss": "^2.0.2",
"@types/yup": "^0.29.11",
"autoprefixer": "^9.8.6",
"postcss": "^7.0.35",
"sass": "^1.32.11",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.1.2"
} }
} }

View File

@ -1,9 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -1,25 +1,15 @@
import React from 'react'; import { useResponse } from '@curi/react-dom';
import logo from './logo.svg';
import './App.css'; import './App.css';
import { DefaultLayout } from './layouts';
function App() { function App() {
const { response } = useResponse();
const { body: Body } = response;
return ( return (
<div className="App"> <DefaultLayout>
<header className="App-header"> <Body response={response} />
<img src={logo} className="App-logo" alt="logo" /> </DefaultLayout>
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
); );
} }

View File

@ -0,0 +1,6 @@
import { ApolloClient, InMemoryCache } from "@apollo/client";
export const client = new ApolloClient({
uri: "/api/graphql",
cache: new InMemoryCache(),
});

271
src/generated/graphql.tsx Normal file
View File

@ -0,0 +1,271 @@
import { gql } from '@apollo/client';
export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
/** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */
DateTime: any;
};
export type CreatePipelineInput = {
projectId: Scalars['String'];
branch: Scalars['String'];
name: Scalars['String'];
workUnitMetadata: WorkUnitMetadataInput;
};
export type CreatePipelineTaskInput = {
pipelineId: Scalars['String'];
commit: Scalars['String'];
units: Array<PipelineUnits>;
};
export type CreateProjectInput = {
name: Scalars['String'];
comment: Scalars['String'];
sshUrl: Scalars['String'];
webUrl?: Maybe<Scalars['String']>;
webHookSecret?: Maybe<Scalars['String']>;
};
export type Hello = {
__typename?: 'Hello';
message: Scalars['String'];
};
export type LogFields = {
__typename?: 'LogFields';
hash: Scalars['String'];
date: Scalars['String'];
message: Scalars['String'];
refs: Scalars['String'];
body: Scalars['String'];
author_name: Scalars['String'];
author_email: Scalars['String'];
tasks: Array<PipelineTask>;
};
export type LogList = {
__typename?: 'LogList';
all: Array<LogFields>;
total: Scalars['Float'];
latest: LogFields;
};
export type Mutation = {
__typename?: 'Mutation';
createProject: Project;
updateProject: Project;
removeProject: Scalars['Float'];
createPipeline: Pipeline;
modifyPipeline: Pipeline;
deletePipeline: Scalars['Float'];
createPipelineTask: PipelineTask;
};
export type MutationCreateProjectArgs = {
project: CreateProjectInput;
};
export type MutationUpdateProjectArgs = {
project: UpdateProjectInput;
};
export type MutationRemoveProjectArgs = {
id: Scalars['String'];
};
export type MutationCreatePipelineArgs = {
pipeline: CreatePipelineInput;
};
export type MutationModifyPipelineArgs = {
Pipeline: UpdatePipelineInput;
id: Scalars['String'];
};
export type MutationDeletePipelineArgs = {
id: Scalars['String'];
};
export type MutationCreatePipelineTaskArgs = {
task: CreatePipelineTaskInput;
};
export type Pipeline = {
__typename?: 'Pipeline';
id: Scalars['ID'];
project: Project;
projectId: Scalars['String'];
branch: Scalars['String'];
name: Scalars['String'];
workUnitMetadata: WorkUnitMetadata;
};
export type PipelineTask = {
__typename?: 'PipelineTask';
id: Scalars['ID'];
pipeline: Pipeline;
pipelineId: Scalars['String'];
commit: Scalars['String'];
units: Array<PipelineUnits>;
logs: Array<PipelineTaskLogs>;
status: TaskStatuses;
startedAt?: Maybe<Scalars['DateTime']>;
endedAt?: Maybe<Scalars['DateTime']>;
};
export type PipelineTaskLogMessage = {
__typename?: 'PipelineTaskLogMessage';
unit?: Maybe<PipelineUnits>;
time: Scalars['DateTime'];
message: Scalars['String'];
isError: Scalars['Boolean'];
};
export type PipelineTaskLogs = {
__typename?: 'PipelineTaskLogs';
unit: PipelineUnits;
status: TaskStatuses;
startedAt?: Maybe<Scalars['DateTime']>;
endedAt?: Maybe<Scalars['DateTime']>;
logs: Scalars['String'];
};
/** 流水线单元 */
export enum PipelineUnits {
Checkout = 'checkout',
InstallDependencies = 'installDependencies',
Test = 'test',
Deploy = 'deploy',
CleanUp = 'cleanUp'
}
export type Project = {
__typename?: 'Project';
id: Scalars['ID'];
name: Scalars['String'];
comment: Scalars['String'];
sshUrl: Scalars['String'];
webUrl?: Maybe<Scalars['String']>;
webHookSecret?: Maybe<Scalars['String']>;
};
export type Query = {
__typename?: 'Query';
hello: Hello;
projects: Array<Project>;
project: Project;
listPipelines: Array<Pipeline>;
findPipeline: Pipeline;
listPipelineTaskByPipelineId: Array<PipelineTask>;
findPipelineTask: PipelineTask;
};
export type QueryProjectArgs = {
id: Scalars['String'];
};
export type QueryListPipelinesArgs = {
projectId?: Maybe<Scalars['String']>;
};
export type QueryFindPipelineArgs = {
id: Scalars['String'];
};
export type QueryListPipelineTaskByPipelineIdArgs = {
pipelineId: Scalars['String'];
};
export type QueryFindPipelineTaskArgs = {
id: Scalars['String'];
};
export type Subscription = {
__typename?: 'Subscription';
listLogsForPipeline: LogList;
pipelineTaskLog: PipelineTaskLogMessage;
pipelineTaskChanged: PipelineTask;
};
export type SubscriptionListLogsForPipelineArgs = {
id: Scalars['String'];
};
export type SubscriptionPipelineTaskLogArgs = {
taskId: Scalars['String'];
};
export type SubscriptionPipelineTaskChangedArgs = {
id: Scalars['String'];
};
/** 任务状态 */
export enum TaskStatuses {
Success = 'success',
Failed = 'failed',
Working = 'working',
Pending = 'pending'
}
export type UpdatePipelineInput = {
projectId: Scalars['String'];
branch: Scalars['String'];
name: Scalars['String'];
workUnitMetadata: WorkUnitMetadataInput;
};
export type UpdateProjectInput = {
name: Scalars['String'];
comment: Scalars['String'];
sshUrl: Scalars['String'];
webUrl?: Maybe<Scalars['String']>;
webHookSecret?: Maybe<Scalars['String']>;
id: Scalars['String'];
};
export type WorkUnit = {
__typename?: 'WorkUnit';
type: PipelineUnits;
scripts: Array<Scalars['String']>;
};
export type WorkUnitInput = {
type: PipelineUnits;
scripts: Array<Scalars['String']>;
};
export type WorkUnitMetadata = {
__typename?: 'WorkUnitMetadata';
version: Scalars['Float'];
units: Array<WorkUnit>;
};
export type WorkUnitMetadataInput = {
version?: Maybe<Scalars['Float']>;
units: Array<WorkUnitInput>;
};

View File

@ -11,3 +11,8 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace; monospace;
} }
a {
text-decoration: none;
color: inherit;
}

View File

@ -1,15 +1,43 @@
import React from 'react'; import React from "react";
import ReactDOM from 'react-dom'; import ReactDOM from "react-dom";
import './index.css'; import "./index.css";
import App from './App'; import "fontsource-roboto";
import reportWebVitals from './reportWebVitals'; import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { client } from "./commons/graphql/client";
import { ApolloProvider } from "@apollo/client";
import { MuiPickersUtilsProvider } from "@material-ui/pickers";
import DateFnsUtils from "@date-io/date-fns";
import zhLocale from "date-fns/locale/zh-CN";
import { createRouterComponent } from "@curi/react-dom";
import { createRouter, announce } from "@curi/router";
import { browser } from "@hickory/browser";
import routes from "./routes";
ReactDOM.render( const router = createRouter(browser, routes, {
<React.StrictMode> sideEffects: [
<App /> announce(({ response }) => {
</React.StrictMode>, return `Navigated to ${response.location.pathname}`;
document.getElementById('root') }),
); ],
external: { client }
});
const Router = createRouterComponent(router);
router.once(() => {
ReactDOM.render(
<React.StrictMode>
<ApolloProvider client={client}>
<MuiPickersUtilsProvider utils={DateFnsUtils} locale={zhLocale}>
<Router>
<App />
</Router>
</MuiPickersUtilsProvider>
</ApolloProvider>
</React.StrictMode>,
document.getElementById("root")
);
});
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log)) // to log results (for example: reportWebVitals(console.log))

154
src/layouts/default.tsx Normal file
View File

@ -0,0 +1,154 @@
import React, { FC } from "react";
import clsx from "clsx";
import {
createStyles,
makeStyles,
useTheme,
Theme,
} from "@material-ui/core/styles";
import Drawer from "@material-ui/core/Drawer";
import AppBar from "@material-ui/core/AppBar";
import Toolbar from "@material-ui/core/Toolbar";
import CssBaseline from "@material-ui/core/CssBaseline";
import Typography from "@material-ui/core/Typography";
import Divider from "@material-ui/core/Divider";
import IconButton from "@material-ui/core/IconButton";
import MenuIcon from "@material-ui/icons/Menu";
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
import { ProjectPanel } from '../projects/project-panel';
const drawerWidth = 240;
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
display: "flex",
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
},
appBarShift: {
marginLeft: drawerWidth,
width: `calc(100% - ${drawerWidth}px)`,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
menuButton: {
marginRight: 36,
},
hide: {
display: "none",
},
drawer: {
width: drawerWidth,
flexShrink: 0,
whiteSpace: "nowrap",
},
drawerOpen: {
width: drawerWidth,
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
},
drawerClose: {
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: "hidden",
width: 0,
},
toolbar: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
},
content: {
flexGrow: 1,
padding: theme.spacing(3),
},
})
);
export const DefaultLayout: FC = ({children}) => {
const classes = useStyles();
const theme = useTheme();
const [open, setOpen] = React.useState(true);
const handleDrawerOpen = () => {
setOpen(true);
};
const handleDrawerClose = () => {
setOpen(false);
};
return (
<div className={classes.root}>
<CssBaseline />
<AppBar
position="fixed"
className={clsx(classes.appBar, {
[classes.appBarShift]: open,
})}
>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
onClick={handleDrawerOpen}
edge="start"
className={clsx(classes.menuButton, {
[classes.hide]: open,
})}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap>
Mini variant drawer
</Typography>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
className={clsx(classes.drawer, {
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
})}
classes={{
paper: clsx({
[classes.drawerOpen]: open,
[classes.drawerClose]: !open,
}),
}}
>
<div className={classes.toolbar}>
<IconButton onClick={handleDrawerClose}>
{theme.direction === "rtl" ? (
<ChevronRightIcon />
) : (
<ChevronLeftIcon />
)}
</IconButton>
</div>
<Divider />
<ProjectPanel />
<Divider />
</Drawer>
<main className={classes.content}>
<div className={classes.toolbar} />
{ children }
</main>
</div>
);
}

1
src/layouts/index.tsx Normal file
View File

@ -0,0 +1 @@
export * from './default';

View File

@ -0,0 +1,203 @@
import { gql, useMutation } from "@apollo/client";
import { Button, LinearProgress, makeStyles, Paper } from "@material-ui/core";
import { Form, Formik, Field, FormikHelpers } from "formik";
import { TextField } from "formik-material-ui";
import { not } from "ramda";
import React, { FC } from "react";
import {
CreateProjectInput,
UpdateProjectInput,
Project,
} from "../generated/graphql";
import * as Yup from "yup";
import { useRouter } from "@curi/react-dom";
type Values = Partial<Project>;
interface Props {
project: Values;
}
const useStyles = makeStyles({
root: {
overflow: "hidden",
},
form: {
margin: 16,
},
});
const CREATE_PROJECT = gql`
mutation CreateProject($input: CreateProjectInput!) {
createProject(project: $input) {
id
name
comment
webUrl
webHookSecret
sshUrl
}
}
`;
const UPDATE_PROJECT = gql`
mutation UpdateProject($input: UpdateProjectInput!) {
updateProject(project: $input) {
id
name
comment
webUrl
webHookSecret
sshUrl
}
}
`;
export const ProjectEditor: FC<Props> = ({ project }) => {
const isCreate = not("id" in project);
const [createProject] = useMutation<{ createProject: Project }>(
CREATE_PROJECT,
{
update(cache, { data }) {
cache.modify({
fields: {
findProjects(exitingProjects = []) {
const newProjectRef = cache.writeFragment({
data: data!.createProject,
fragment: gql`
fragment newProject on Project {
id
name
comment
webUrl
sshUrl
webHookSecret
}
`,
});
return [newProjectRef, ...exitingProjects];
},
},
});
},
}
);
const [updateProject] = useMutation(UPDATE_PROJECT);
const router = useRouter();
const submitForm = async (
values: Values,
formikHelpers: FormikHelpers<Values>
) => {
try {
let projectId: string | undefined = project.id;
if (isCreate) {
await createProject({
variables: {
input: values,
},
}).then(({ data }) => (projectId = data!.createProject.id));
} else {
await updateProject({
variables: {
id: (project as Project).id,
input: values,
},
});
}
router.navigate({
url: router.url({
name: "project-detail",
params: {
projectId,
},
}),
method: "replace",
});
} finally {
formikHelpers.setSubmitting(false);
}
};
const classes = useStyles();
return (
<Paper className={classes.root}>
<Formik
initialValues={project}
validationSchema={Yup.object({
name: Yup.string()
.max(32, "Must be 32 characters or less")
.required("Required"),
comment: Yup.string()
.max(32, "Must be 32 characters or less")
.required("Required"),
webUrl: Yup.string()
.matches(
/^(https?:\/\/)?([\da-z.-]+\.[a-z.]{2,6}|[\d.]+)([\\/:?=&#]{1}[\da-z.-]+)*[/\\?]?$/i,
"Enter correct url!"
)
.max(256, "Must be 256 characters or less")
.required("Required"),
sshUrl: Yup.string()
.matches(
/^(?:ssh:\/\/)?(?:[\w\d-_]+@)?(?:[\w\d-_]+\.)*\w{2,10}(?::\d{1,5})?(?:\/[\w\d-_.]+)*$/,
"Enter correct url!"
)
.max(256, "Must be 256 characters or less")
.required("Required"),
webHookSecret: Yup.string()
.max(16, "Must be 16 characters or less")
.required("Required"),
})}
onSubmit={submitForm}
>
{({ submitForm, isSubmitting }) => (
<Form className={classes.form}>
<Field component={TextField} name="name" label="Name" fullWidth />
<Field
component={TextField}
name="comment"
label="Comment"
fullWidth
margin="normal"
/>
<Field
component={TextField}
name="webUrl"
label="Project URL"
placeholder="The project website url"
fullWidth
margin="normal"
/>
<Field
component={TextField}
name="sshUrl"
label="Git SSH URL"
placeholder="The Git remote SSH URL"
fullWidth
margin="normal"
/>
<Field
component={TextField}
name="webHookSecret"
label="Webhook Secret"
fullWidth
margin="normal"
/>
{isSubmitting && <LinearProgress />}
<Button
variant="contained"
color="primary"
disabled={isSubmitting}
onClick={submitForm}
fullWidth
>
Submit
</Button>
</Form>
)}
</Formik>
</Paper>
);
};

View File

@ -0,0 +1,82 @@
import { gql, useQuery } from '@apollo/client';
import { Link, useRouter } from '@curi/react-dom';
import { Box, List, ListItem } from '@material-ui/core';
import { makeStyles, Theme, createStyles } from '@material-ui/core';
import { find, propEq } from 'ramda';
import React, { useState, FC, useEffect } from 'react';
import { Project } from '../generated/graphql';
import { ListItemText } from '@material-ui/core';
import { Button } from '@material-ui/core';
import { AddBox } from '@material-ui/icons';
const PROJECTS = gql`
query Projects {
projects {
id
name
comment
sshUrl
webUrl
webHookSecret
}
}
`;
const useStyles = makeStyles((theme: Theme) =>
createStyles({
})
);
export function ProjectPanel() {
return (
<section>
<Box m={2}>
<Link
name="create-project"
>
<Button
variant="contained"
color="primary"
title="New Project"
startIcon={<AddBox />}
>
New Project
</Button>
</Link>
</Box>
<ProjectList />
</section>
);
}
const ProjectList: FC<{}> = () => {
const { data, refetch } = useQuery<{
projects: Project[];
}>(PROJECTS);
const projects = data?.projects;
const [currentProject, setCurrentProject] = useState<Project | undefined>(
undefined
);
const { current } = useRouter();
useEffect(() => {
const currId = current()?.response?.params.projectId;
console.log(currId);
setCurrentProject(find(propEq("id", currId), projects ?? []));
}, [current, projects]);
const items = projects?.map((item) => (
<Link
name="edit-project"
params={{ projectId: item.id }}
key={item.id}
onNav={() => setCurrentProject(item)}
>
<ListItem button>
<ListItemText primary={item.name} secondary={item.comment} />
</ListItem>
</Link>
));
return <List>{items}</List>;
};

14
src/projects/queries.ts Normal file
View File

@ -0,0 +1,14 @@
import { gql } from '@apollo/client';
export const PROJECT = gql`
query Project($id:String!) {
project(id: $id) {
id
name
comment
webUrl
sshUrl
webHookSecret
}
}
`

61
src/routes.tsx Normal file
View File

@ -0,0 +1,61 @@
import { ApolloClient, InMemoryCache } from "@apollo/client";
import { prepareRoutes } from "@curi/router";
import { omit } from 'ramda';
import { CreateProjectInput, Project } from './generated/graphql';
import { ProjectEditor } from './projects/project-editor';
import { PROJECT } from './projects/queries';
export default prepareRoutes([
{
name: "dashboard",
path: "",
respond() {
return { body: () => <div>DashBoard</div> };
},
},
{
name: "create-project",
path: "projects/create",
respond({ resolved }) {
const input: CreateProjectInput = {
name: "",
comment: "",
webHookSecret: "",
sshUrl: "",
webUrl: "",
};
return { body: () => <ProjectEditor project={input} /> };
},
},
{
name: "edit-project",
path: "projects/:projectId/edit",
async resolve(matched, { client }: { client: ApolloClient<InMemoryCache> }) {
const { data } = await client.query<{ project: Project }>({
query: PROJECT,
variables: { id: matched?.params.projectId },
});
return {
body: () => (
<ProjectEditor project={omit(["__typename"], data.project)} />
),
};
},
respond({ resolved }) {
return resolved;
},
},
{
name: "project-detail",
path: "projects/:projectId",
resolve() {
const body = import(
/* webpackChunkName: "article-editor" */ "./projects/project-panel"
).then((m) => m.ProjectPanel);
return body;
},
respond({ resolved }) {
return { body: resolved };
},
},
]);