Skip to content

Commit df0acc0

Browse files
authored
feat(web_console): activity (#393)
1 parent 539756d commit df0acc0

File tree

26 files changed

+3992
-1623
lines changed

26 files changed

+3992
-1623
lines changed

web_console/README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# Fedlearner™ Web Console
1+
Fedlearner™ Web Console
2+
=======================
23

34
the web console of [Fedlearner][fedlearner].
45

@@ -64,16 +65,33 @@ for testing with coverage report:
6465
npm run cov
6566
```
6667

67-
------------------------------------------------------------------------------
68+
## Release
69+
70+
> release action **should only** be executed by maintainers
71+
72+
make a new release with tagged commit, `m.m.p` **must** follow [semver][semver] standard:
73+
74+
```
75+
# make sure you are at master branch
76+
npm run release -- --release-as m.m.p
77+
git push origin master --tags
78+
```
79+
80+
then create a new release at [Github][new_github_release] with latest content from `CHANGELOG.md`.
81+
82+
83+
6884
[conventionalcommits]: https://www.conventionalcommits.org/en/v1.0.0/#summary
6985
[docker]: https://docs.docker.com/get-docker
7086
[fedlearner]: https://github.com/bytedance/fedlearner
7187
[koa]: https://koajs.com
7288
[mariadb]: https://downloads.mariadb.org
7389
[minikube]: https://minikube.sigs.k8s.io
7490
[mysql]: https://dev.mysql.com/downloads/mysql
91+
[new_github_release]: https://github.com/bytedance/fedlearner/releases/new
7592
[next]: https://nextjs.org/docs
7693
[node]: https://nodejs.org/en/about/releases
7794
[nvm]: https://github.com/nvm-sh/nvm
95+
[semver]: https://semver.org
7896
[sequelize]: https://sequelize.org
7997
[zeit_ui]: https://react.zeit-ui.co/zh-cn/components/text

web_console/api/activity.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const router = require('@koa/router')();
2+
const SessionMiddleware = require('../middlewares/session');
3+
const PaginationMiddleware = require('../middlewares/pagination');
4+
const docker = require('../libs/docker');
5+
6+
/**
7+
* Convert images from Docker Hub to releases
8+
*
9+
* @param {Array<Object>} images - ref: https://hub.docker.com/v2/repositories/fedlearner/fedlearner-web-console/tags/
10+
* @return {Array<Object>} - checkout `tests/fixtures/activity.js` for schema detail
11+
*/
12+
function mapImageToRelease(images) {
13+
return images.map(x => ({
14+
id: x.images[0].digest,
15+
type: 'release',
16+
creator: 'bytedance',
17+
created_at: x.tag_last_pushed,
18+
ctx: {
19+
docker: x,
20+
// fake github data
21+
github: {
22+
html_url: `https://github.com/bytedance/fedlearner/releases/tag/${x.name}`,
23+
tag_name: x.name,
24+
published_at: x.tag_last_pushed,
25+
author: {
26+
login: 'bytedance',
27+
avatar_url: 'https://avatars3.githubusercontent.com/u/4158466?v=4',
28+
html_url: 'https://github.com/bytedance',
29+
},
30+
},
31+
},
32+
}));
33+
}
34+
35+
router.get('/api/v1/activities', SessionMiddleware, PaginationMiddleware, async (ctx) => {
36+
let page = 1;
37+
let pageSize = 10;
38+
39+
if (ctx.pagination.limit > 0 && ctx.pagination.offset >= 0) {
40+
page = ctx.pagination.offset / ctx.pagination.limit + 1;
41+
pageSize = ctx.pagination.limit;
42+
}
43+
44+
// 'v' is used to filter standard versions
45+
const { count, results } = await docker.listImageTags('fedlearner/fedlearner-web-console', 'v', page, pageSize);
46+
ctx.body = { count, data: mapImageToRelease(results) };
47+
});
48+
49+
module.exports = router;

web_console/api/deployment.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Common proxy of Kubernetes Deployment API
3+
*/
4+
5+
const router = require('@koa/router')();
6+
const SessionMiddleware = require('../middlewares/session');
7+
const k8s = require('../libs/k8s');
8+
9+
router.get('/api/v1/deployments', SessionMiddleware, async (ctx) => {
10+
const res = await k8s.listDeployments();
11+
ctx.body = { data: res.deployments.items };
12+
});
13+
14+
router.get('/api/v1/deployments/:name', SessionMiddleware, async (ctx) => {
15+
const { deployment: data } = await k8s.getDeployment(ctx.params.name);
16+
ctx.body = { data };
17+
});
18+
19+
router.put('/api/v1/deployments/:name', SessionMiddleware, async (ctx) => {
20+
const { deployment: data } = await k8s.updateDeployment(ctx.request.body);
21+
ctx.body = { data };
22+
});
23+
24+
module.exports = router;

web_console/api/job.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const FederationClient = require('../rpc/client');
99
const getConfig = require('../utils/get_confg');
1010
const checkParseJson = require('../utils/check_parse_json');
1111
const { clientValidateJob, clientGenerateYaml } = require('../utils/job_builder');
12-
const { client } = require('../libs/k8s');
1312

1413
const config = getConfig({
1514
NAMESPACE: process.env.NAMESPACE,
@@ -435,7 +434,7 @@ router.post('/api/v1/job/:id/update', SessionMiddleware, async (ctx) => {
435434
if (old_job.status === 'started' && new_job.status === 'stopped') {
436435
flapp = (await k8s.getFLApp(namespace, new_job.name)).flapp;
437436
pods = (await k8s.getFLAppPods(namespace, new_job.name)).pods;
438-
old_job.k8s_meta_snapshot = JSON.stringify({flapp, pods});
437+
old_job.k8s_meta_snapshot = JSON.stringify({ flapp, pods });
439438
await k8s.deleteFLApp(namespace, new_job.name);
440439
} else if (old_job.status === 'stopped' && new_job.status === 'started') {
441440
const clientYaml = clientGenerateYaml(clientFed, new_job, clientTicket);

web_console/api/user.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,38 @@ router.get('/api/v1/user', SessionMiddleware, async (ctx) => {
99
ctx.body = { data: ctx.session.user };
1010
});
1111

12+
router.put('/api/v1/user', SessionMiddleware, async (ctx) => {
13+
const { body } = ctx.request;
14+
15+
const user = await User.findByPk(ctx.session.user.id);
16+
if (!user) {
17+
ctx.status = 404;
18+
ctx.body = {
19+
error: 'User not found',
20+
};
21+
return;
22+
}
23+
24+
const fields = ['password', 'name', 'tel', 'email', 'avatar', 'is_admin'].reduce((total, current) => {
25+
const value = body[current];
26+
if (value) {
27+
total[current] = current === 'password' ? encrypt(value) : value;
28+
}
29+
return total;
30+
}, {});
31+
32+
await user.update(fields);
33+
ctx.session.user = {
34+
id: user.id,
35+
username: user.username,
36+
name: user.name,
37+
tel: user.tel,
38+
is_admin: user.is_admin,
39+
};
40+
ctx.session.manuallyCommit();
41+
ctx.body = { data: user };
42+
});
43+
1244
router.get('/api/v1/users', SessionMiddleware, AdminMiddleware, async (ctx) => {
1345
const data = await User.findAll({ paranoid: false });
1446
ctx.body = { data };
@@ -77,6 +109,18 @@ router.put('/api/v1/users/:id', SessionMiddleware, AdminMiddleware, async (ctx)
77109
}, {});
78110

79111
await user.update(fields);
112+
113+
if (user.id === ctx.session.user.id) {
114+
ctx.session.user = {
115+
id: user.id,
116+
username: user.username,
117+
name: user.name,
118+
tel: user.tel,
119+
is_admin: user.is_admin,
120+
};
121+
ctx.session.manuallyCommit();
122+
}
123+
80124
ctx.body = { data: user };
81125
});
82126

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import css from 'styled-jsx/css';
3+
import { Avatar, Link, Text, useTheme } from '@zeit-ui/react';
4+
import { humanizeDuration } from '../utils/time';
5+
6+
function useStyles(theme) {
7+
return css`
8+
.event {
9+
display: flex;
10+
align-items: center;
11+
padding: 10px 0px;
12+
border-bottom: 1px solid ${theme.palette.accents_2};
13+
font-size: 14px;
14+
}
15+
`;
16+
}
17+
18+
function renderRelease(activity) {
19+
const { creator, created_at, ctx } = activity;
20+
const { author, html_url, tag_name } = ctx.github;
21+
22+
return (
23+
<>
24+
<Link href={author.html_url} target="_blank" rel="noopenner noreferer">
25+
<Avatar
26+
src={author.avatar_url}
27+
alt={`${creator} Avatar`}
28+
/>
29+
</Link>
30+
<div style={{ flex: 1, display: 'flex', alignItems: 'center', margin: '0 0 0 10px' }}>
31+
<Text b>
32+
<Link href={author.html_url} target="_blank" rel="noopenner noreferer">{creator}</Link>
33+
</Text>
34+
<Text style={{ margin: '0 4px' }}>released version</Text>
35+
<Text b>
36+
<Link href={html_url} target="_blank" rel="noopenner noreferer">{tag_name}</Link>
37+
</Text>
38+
</div>
39+
<Text type="secondary">{humanizeDuration(created_at)}</Text>
40+
</>
41+
);
42+
}
43+
44+
export default function ActivityListItem({ activity }) {
45+
const theme = useTheme();
46+
const styles = useStyles(theme);
47+
const { type } = activity;
48+
49+
return (
50+
<div className="event">
51+
{type === 'release' && renderRelease(activity)}
52+
{/* render other type of activity here */}
53+
<style jsx>{styles}</style>
54+
</div>
55+
);
56+
}

web_console/components/EventListItem.jsx

Lines changed: 0 additions & 43 deletions
This file was deleted.

0 commit comments

Comments
 (0)