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

Create Activities page to display all deployments in one place #382

Merged
merged 8 commits into from
Apr 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions openapi/v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,12 @@ paths:
schema:
type: boolean
description: Own deployments.
- in: query
name: production_only
required: false
schema:
type: boolean
description: Return the deployments for the production environment.
- in: query
name: from
required: false
Expand Down
4 changes: 4 additions & 0 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Repo from "./views/repo"
import Deployment from "./views/deployment"
import Settings from "./views/settings"
import Members from "./views/members"
import Activities from "./views/activities"

function App(): JSX.Element {
return (
Expand All @@ -31,6 +32,9 @@ function App(): JSX.Element {
<Route path="/members">
<Members />
</Route>
<Route path="/activities">
<Activities />
</Route>
<Route path="/">
<Home />
</Route>
Expand Down
6 changes: 3 additions & 3 deletions ui/src/apis/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,16 +150,16 @@ function mapDeploymentStatusToString(status: DeploymentStatusEnum): string {

}

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

const fromParam = (from)? `from=${from.toISOString()}` : ""
const toParam = (to)? `&to=to.toISOString()` : ""
const toParam = (to)? `&to=${to.toISOString()}` : ""
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix the bug


const deployments: Deployment[] = await _fetch(`${instance}/api/v1/search/deployments?statuses=${ss.join(",")}&owned=${owned}&${fromParam}&${toParam}&page=${page}&per_page=${perPage}`, {
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}`, {
headers,
credentials: 'same-origin',
})
Expand Down
4 changes: 2 additions & 2 deletions ui/src/components/DeploymentRefCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ interface DeploymentRefCodeProps {
export default function DeploymentRefCode(props: DeploymentRefCodeProps): JSX.Element {
let ref: string
if (props.deployment.type === DeploymentType.Commit) {
ref = props.deployment.ref.substr(0, 7)
ref = props.deployment.ref.substring(0, 7)
} else if (props.deployment.type === DeploymentType.Branch && props.deployment.sha !== "") {
ref = `${props.deployment.ref}(${props.deployment.sha.substr(0, 7)})`
ref = `${props.deployment.ref}(${props.deployment.sha.substring(0, 7)})`
} else {
ref = props.deployment.ref
}
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/RecentActivities.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface DeploymentListProps {
deployments: Deployment[]
}

// TODO: Move the component into the main view.
function DeploymentList(props: DeploymentListProps): JSX.Element {
return <List
dataSource={props.deployments}
Expand Down
23 changes: 23 additions & 0 deletions ui/src/components/partials/deploymentStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { DeploymentStatusEnum } from "../../models"

// https://ant.design/components/timeline/#Timeline.Item
export const getStatusColor = (status: DeploymentStatusEnum): string => {
switch (status) {
case DeploymentStatusEnum.Waiting:
return "gray"
case DeploymentStatusEnum.Created:
return "purple"
case DeploymentStatusEnum.Queued:
return "purple"
case DeploymentStatusEnum.Running:
return "purple"
case DeploymentStatusEnum.Success:
return "green"
case DeploymentStatusEnum.Failure:
return "red"
case DeploymentStatusEnum.Canceled:
return "gray"
default:
return "gray"
}
}
3 changes: 3 additions & 0 deletions ui/src/components/partials/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { getStatusColor } from "./deploymentStatus"

export { getStatusColor }
66 changes: 66 additions & 0 deletions ui/src/redux/activities.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"

import {
searchDeployments as _searchDeployments,
} from "../apis"
import { Deployment, } from "../models"

export const perPage = 30

interface ActivitiesState {
loading: boolean
deployments: Deployment[]
page: number
}

const initialState: ActivitiesState = {
loading: false,
deployments: [],
page: 1,
}

export const searchDeployments = createAsyncThunk<
Deployment[], {
start?: Date,
end?: Date,
productionOnly: boolean,
}, {
state: { activities: ActivitiesState }
}>(
"activities/searchDeployments",
async ({start, end, productionOnly}, { getState, rejectWithValue }) => {
const {page} = getState().activities
try {
return await _searchDeployments([], false, productionOnly, start, end, page, perPage)
} catch (e) {
return rejectWithValue(e)
}
}
)

export const activitiesSlice = createSlice({
name: "activities",
initialState,
reducers: {
increasePage: (state) => {
state.page = state.page + 1
},
decreasePage: (state) => {
state.page = state.page - 1
},
},
extraReducers: builder => {
builder
.addCase(searchDeployments.pending, (state) => {
state.loading = true
state.deployments = []
})
.addCase(searchDeployments.fulfilled, (state, action) => {
state.loading = false
state.deployments = action.payload
})
.addCase(searchDeployments.rejected, (state) => {
state.loading = false
})
}
})
2 changes: 1 addition & 1 deletion ui/src/redux/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export const searchDeployments = createAsyncThunk<Deployment[], void, { state: {
DeploymentStatusEnum.Created,
DeploymentStatusEnum.Queued,
DeploymentStatusEnum.Running,
], false)
], false, false)
return deployments
} catch (e) {
return rejectWithValue(e)
Expand Down
2 changes: 2 additions & 0 deletions ui/src/redux/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { repoSettingsSlice } from "./repoSettings"
import { settingsSlice } from "./settings"
import { deploymentSlice } from "./deployment"
import { membersSlice } from "./members"
import { activitiesSlice } from './activities'

export const store = configureStore({
reducer: {
Expand All @@ -25,6 +26,7 @@ export const store = configureStore({
settings: settingsSlice.reducer,
deployment: deploymentSlice.reducer,
members: membersSlice.reducer,
activities: activitiesSlice.reducer,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
serializableCheck: false
Expand Down
38 changes: 38 additions & 0 deletions ui/src/views/activities/ActivityHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Timeline, Typography } from 'antd'
import moment from "moment"

import { Deployment } from "../../models"
import DeploymentStatusBadge from "../../components/DeploymentStatusBadge"
import UserAvatar from '../../components/UserAvatar'
import DeploymentRefCode from '../../components/DeploymentRefCode'
import { getStatusColor } from "../../components/partials"

const { Text } = Typography

export interface ActivityHistoryProps {
deployments: Deployment[]
}

export default function ActivityHistory(props: ActivityHistoryProps): JSX.Element {
return (
<Timeline>
{props.deployments.map((d, idx) => {
return (
<Timeline.Item key={idx} color={getStatusColor(d.status)}>
<p>
<Text strong>
{`${d.repo?.namespace} / ${d.repo?.name}`}
</Text>&nbsp;
<a href={`/${d.repo?.namespace}/${d.repo?.name}/deployments/${d.number}`}>
#{d.number}
</a>
</p>
<p>
<UserAvatar user={d.deployer} /> deployed <DeploymentRefCode deployment={d}/> to <Text strong>{d.env}</Text> on {moment(d.createdAt).format("LLL")} <DeploymentStatusBadge deployment={d}/>
</p>
</Timeline.Item>
)
})}
</Timeline>
)
}
63 changes: 63 additions & 0 deletions ui/src/views/activities/SearchActivities.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Row, Col, Form, DatePicker, Button, Switch } from "antd"
import moment, { Moment } from "moment"

export interface SearchActivitiesValues {
period?: [Moment, Moment]
productionOnly?: boolean
}

export interface SearchActivitiesProps {
initialValues?: SearchActivitiesValues
onClickSearch(values: SearchActivitiesValues): void
Comment on lines +10 to +11
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It defines only the initial value and action for clicking the search button as a property of the component.

}

export default function SearchActivities(props: SearchActivitiesProps): JSX.Element {
const content = (
<>
<Form.Item label="Period" name="period">
<DatePicker.RangePicker
format="YYYY-MM-DD HH:mm"
showTime={{
showSecond: false,
defaultValue: [moment().hour(0).minute(0), moment().hour(23).minute(59)],
}}
/>
</Form.Item>
<Form.Item label="Production" name="productionOnly" valuePropName="checked">
<Switch size="small" />
</Form.Item>
<Form.Item >
<Button
htmlType="submit"
type="primary"
>
Search
</Button>
</Form.Item>
</>
)
return (
<Row>
{/* Mobile view */}
<Col span={24} lg={0}>
<Form
layout="horizontal"
initialValues={props.initialValues}
onFinish={props.onClickSearch}
>
{content}
</Form>
</Col>
{/* Laptop */}
<Col span={0} lg={24}>
<Form
layout="inline"
initialValues={props.initialValues}
onFinish={props.onClickSearch}
>
{content}
</Form>
</Col>
</Row>
)
}
Loading