Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit 42071a8

Browse files
author
Noah Lee
authored
Create Activities page to display all deployments in one place (#382)
* Add the activities view * Add the `production_only` param to the query
1 parent 21de3b2 commit 42071a8

15 files changed

+327
-29
lines changed

Diff for: openapi/v1.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -1344,6 +1344,12 @@ paths:
13441344
schema:
13451345
type: boolean
13461346
description: Own deployments.
1347+
- in: query
1348+
name: production_only
1349+
required: false
1350+
schema:
1351+
type: boolean
1352+
description: Return the deployments for the production environment.
13471353
- in: query
13481354
name: from
13491355
required: false

Diff for: ui/src/App.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Repo from "./views/repo"
1010
import Deployment from "./views/deployment"
1111
import Settings from "./views/settings"
1212
import Members from "./views/members"
13+
import Activities from "./views/activities"
1314

1415
function App(): JSX.Element {
1516
return (
@@ -31,6 +32,9 @@ function App(): JSX.Element {
3132
<Route path="/members">
3233
<Members />
3334
</Route>
35+
<Route path="/activities">
36+
<Activities />
37+
</Route>
3438
<Route path="/">
3539
<Home />
3640
</Route>

Diff for: ui/src/apis/deployment.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -150,16 +150,16 @@ function mapDeploymentStatusToString(status: DeploymentStatusEnum): string {
150150

151151
}
152152

153-
export const searchDeployments = async (statuses: DeploymentStatusEnum[], owned: boolean, from?: Date, to?: Date, page = 1, perPage = 30): Promise<Deployment[]> => {
153+
export const searchDeployments = async (statuses: DeploymentStatusEnum[], owned: boolean, productionOnly: boolean, from?: Date, to?: Date, page = 1, perPage = 30): Promise<Deployment[]> => {
154154
const ss: string[] = []
155155
statuses.forEach((status) => {
156156
ss.push(mapDeploymentStatusToString(status))
157157
})
158158

159159
const fromParam = (from)? `from=${from.toISOString()}` : ""
160-
const toParam = (to)? `&to=to.toISOString()` : ""
160+
const toParam = (to)? `&to=${to.toISOString()}` : ""
161161

162-
const deployments: Deployment[] = await _fetch(`${instance}/api/v1/search/deployments?statuses=${ss.join(",")}&owned=${owned}&${fromParam}&${toParam}&page=${page}&per_page=${perPage}`, {
162+
const deployments: Deployment[] = await _fetch(`${instance}/api/v1/search/deployments?statuses=${ss.join(",")}&owned=${owned}&production_only=${productionOnly}&${fromParam}&${toParam}&page=${page}&per_page=${perPage}`, {
163163
headers,
164164
credentials: 'same-origin',
165165
})

Diff for: ui/src/components/DeploymentRefCode.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ interface DeploymentRefCodeProps {
1111
export default function DeploymentRefCode(props: DeploymentRefCodeProps): JSX.Element {
1212
let ref: string
1313
if (props.deployment.type === DeploymentType.Commit) {
14-
ref = props.deployment.ref.substr(0, 7)
14+
ref = props.deployment.ref.substring(0, 7)
1515
} else if (props.deployment.type === DeploymentType.Branch && props.deployment.sha !== "") {
16-
ref = `${props.deployment.ref}(${props.deployment.sha.substr(0, 7)})`
16+
ref = `${props.deployment.ref}(${props.deployment.sha.substring(0, 7)})`
1717
} else {
1818
ref = props.deployment.ref
1919
}

Diff for: ui/src/components/RecentActivities.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ interface DeploymentListProps {
2929
deployments: Deployment[]
3030
}
3131

32+
// TODO: Move the component into the main view.
3233
function DeploymentList(props: DeploymentListProps): JSX.Element {
3334
return <List
3435
dataSource={props.deployments}

Diff for: ui/src/components/partials/deploymentStatus.tsx

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { DeploymentStatusEnum } from "../../models"
2+
3+
// https://ant.design/components/timeline/#Timeline.Item
4+
export const getStatusColor = (status: DeploymentStatusEnum): string => {
5+
switch (status) {
6+
case DeploymentStatusEnum.Waiting:
7+
return "gray"
8+
case DeploymentStatusEnum.Created:
9+
return "purple"
10+
case DeploymentStatusEnum.Queued:
11+
return "purple"
12+
case DeploymentStatusEnum.Running:
13+
return "purple"
14+
case DeploymentStatusEnum.Success:
15+
return "green"
16+
case DeploymentStatusEnum.Failure:
17+
return "red"
18+
case DeploymentStatusEnum.Canceled:
19+
return "gray"
20+
default:
21+
return "gray"
22+
}
23+
}

Diff for: ui/src/components/partials/index.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { getStatusColor } from "./deploymentStatus"
2+
3+
export { getStatusColor }

Diff for: ui/src/redux/activities.tsx

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
2+
3+
import {
4+
searchDeployments as _searchDeployments,
5+
} from "../apis"
6+
import { Deployment, } from "../models"
7+
8+
export const perPage = 30
9+
10+
interface ActivitiesState {
11+
loading: boolean
12+
deployments: Deployment[]
13+
page: number
14+
}
15+
16+
const initialState: ActivitiesState = {
17+
loading: false,
18+
deployments: [],
19+
page: 1,
20+
}
21+
22+
export const searchDeployments = createAsyncThunk<
23+
Deployment[], {
24+
start?: Date,
25+
end?: Date,
26+
productionOnly: boolean,
27+
}, {
28+
state: { activities: ActivitiesState }
29+
}>(
30+
"activities/searchDeployments",
31+
async ({start, end, productionOnly}, { getState, rejectWithValue }) => {
32+
const {page} = getState().activities
33+
try {
34+
return await _searchDeployments([], false, productionOnly, start, end, page, perPage)
35+
} catch (e) {
36+
return rejectWithValue(e)
37+
}
38+
}
39+
)
40+
41+
export const activitiesSlice = createSlice({
42+
name: "activities",
43+
initialState,
44+
reducers: {
45+
increasePage: (state) => {
46+
state.page = state.page + 1
47+
},
48+
decreasePage: (state) => {
49+
state.page = state.page - 1
50+
},
51+
},
52+
extraReducers: builder => {
53+
builder
54+
.addCase(searchDeployments.pending, (state) => {
55+
state.loading = true
56+
state.deployments = []
57+
})
58+
.addCase(searchDeployments.fulfilled, (state, action) => {
59+
state.loading = false
60+
state.deployments = action.payload
61+
})
62+
.addCase(searchDeployments.rejected, (state) => {
63+
state.loading = false
64+
})
65+
}
66+
})

Diff for: ui/src/redux/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export const searchDeployments = createAsyncThunk<Deployment[], void, { state: {
8888
DeploymentStatusEnum.Created,
8989
DeploymentStatusEnum.Queued,
9090
DeploymentStatusEnum.Running,
91-
], false)
91+
], false, false)
9292
return deployments
9393
} catch (e) {
9494
return rejectWithValue(e)

Diff for: ui/src/redux/store.ts

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { repoSettingsSlice } from "./repoSettings"
1111
import { settingsSlice } from "./settings"
1212
import { deploymentSlice } from "./deployment"
1313
import { membersSlice } from "./members"
14+
import { activitiesSlice } from './activities'
1415

1516
export const store = configureStore({
1617
reducer: {
@@ -25,6 +26,7 @@ export const store = configureStore({
2526
settings: settingsSlice.reducer,
2627
deployment: deploymentSlice.reducer,
2728
members: membersSlice.reducer,
29+
activities: activitiesSlice.reducer,
2830
},
2931
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
3032
serializableCheck: false

Diff for: ui/src/views/activities/ActivityHistory.tsx

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Timeline, Typography } from 'antd'
2+
import moment from "moment"
3+
4+
import { Deployment } from "../../models"
5+
import DeploymentStatusBadge from "../../components/DeploymentStatusBadge"
6+
import UserAvatar from '../../components/UserAvatar'
7+
import DeploymentRefCode from '../../components/DeploymentRefCode'
8+
import { getStatusColor } from "../../components/partials"
9+
10+
const { Text } = Typography
11+
12+
export interface ActivityHistoryProps {
13+
deployments: Deployment[]
14+
}
15+
16+
export default function ActivityHistory(props: ActivityHistoryProps): JSX.Element {
17+
return (
18+
<Timeline>
19+
{props.deployments.map((d, idx) => {
20+
return (
21+
<Timeline.Item key={idx} color={getStatusColor(d.status)}>
22+
<p>
23+
<Text strong>
24+
{`${d.repo?.namespace} / ${d.repo?.name}`}
25+
</Text>&nbsp;
26+
<a href={`/${d.repo?.namespace}/${d.repo?.name}/deployments/${d.number}`}>
27+
#{d.number}
28+
</a>
29+
</p>
30+
<p>
31+
<UserAvatar user={d.deployer} /> deployed <DeploymentRefCode deployment={d}/> to <Text strong>{d.env}</Text> on {moment(d.createdAt).format("LLL")} <DeploymentStatusBadge deployment={d}/>
32+
</p>
33+
</Timeline.Item>
34+
)
35+
})}
36+
</Timeline>
37+
)
38+
}

Diff for: ui/src/views/activities/SearchActivities.tsx

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Row, Col, Form, DatePicker, Button, Switch } from "antd"
2+
import moment, { Moment } from "moment"
3+
4+
export interface SearchActivitiesValues {
5+
period?: [Moment, Moment]
6+
productionOnly?: boolean
7+
}
8+
9+
export interface SearchActivitiesProps {
10+
initialValues?: SearchActivitiesValues
11+
onClickSearch(values: SearchActivitiesValues): void
12+
}
13+
14+
export default function SearchActivities(props: SearchActivitiesProps): JSX.Element {
15+
const content = (
16+
<>
17+
<Form.Item label="Period" name="period">
18+
<DatePicker.RangePicker
19+
format="YYYY-MM-DD HH:mm"
20+
showTime={{
21+
showSecond: false,
22+
defaultValue: [moment().hour(0).minute(0), moment().hour(23).minute(59)],
23+
}}
24+
/>
25+
</Form.Item>
26+
<Form.Item label="Production" name="productionOnly" valuePropName="checked">
27+
<Switch size="small" />
28+
</Form.Item>
29+
<Form.Item >
30+
<Button
31+
htmlType="submit"
32+
type="primary"
33+
>
34+
Search
35+
</Button>
36+
</Form.Item>
37+
</>
38+
)
39+
return (
40+
<Row>
41+
{/* Mobile view */}
42+
<Col span={24} lg={0}>
43+
<Form
44+
layout="horizontal"
45+
initialValues={props.initialValues}
46+
onFinish={props.onClickSearch}
47+
>
48+
{content}
49+
</Form>
50+
</Col>
51+
{/* Laptop */}
52+
<Col span={0} lg={24}>
53+
<Form
54+
layout="inline"
55+
initialValues={props.initialValues}
56+
onFinish={props.onClickSearch}
57+
>
58+
{content}
59+
</Form>
60+
</Col>
61+
</Row>
62+
)
63+
}

0 commit comments

Comments
 (0)