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

Provide the browser notification for events #268

Merged
merged 2 commits into from
Dec 18, 2021
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
141 changes: 110 additions & 31 deletions ui/src/redux/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,6 @@ const initialState: MainState = {
reviews: [],
}

const runningDeploymentStatus: DeploymentStatusEnum[] = [
DeploymentStatusEnum.Waiting,
DeploymentStatusEnum.Created,
DeploymentStatusEnum.Running,
]

export const apiMiddleware: Middleware = (api: MiddlewareAPI) => (
next
) => (action) => {
Expand Down Expand Up @@ -81,18 +75,28 @@ export const init = createAsyncThunk<User, void, { state: { main: MainState } }>
}
)

/**
* Search all processing deployments that the user can access.
*/
export const searchDeployments = createAsyncThunk<Deployment[], void, { state: { main: MainState } }>(
"main/searchDeployments",
async (_, { rejectWithValue }) => {
try {
const deployments = await _searchDeployments(runningDeploymentStatus, false)
const deployments = await _searchDeployments([
DeploymentStatusEnum.Waiting,
DeploymentStatusEnum.Created,
DeploymentStatusEnum.Running,
], false)
return deployments
} catch (e) {
return rejectWithValue(e)
}
}
)

/**
* Search all reviews has requested.
*/
export const searchReviews = createAsyncThunk<Review[], void, { state: { main: MainState } }>(
"main/searchReviews",
async (_, { rejectWithValue }) => {
Expand All @@ -117,6 +121,87 @@ export const fetchLicense = createAsyncThunk<License, void, { state: { main: Mai
}
)

const notify = (title: string, options?: NotificationOptions) => {
if (!("Notification" in window)) {
console.log("This browser doesn't support the notification.")
return
}

if (Notification.permission === "default") {
Notification.requestPermission()
}

new Notification(title, options)
}

/**
* The browser notifies only the user who triggers the deployment.
*/
export const notifyDeploymentEvent = createAsyncThunk<void, Event, { state: { main: MainState } }>(
"main/notifyDeploymentEvent",
async (event, { getState }) => {
const { user } = getState().main

if (event.kind !== EventKindEnum.Deployment) {
return
}

if (event.deployment?.deployer?.id !== user?.id) {
return
}

if (event.type === EventTypeEnum.Created) {
notify(`New Deployment #${event.deployment?.number}`, {
icon: "/logo192.png",
body: `Start to deploy ${event.deployment?.ref.substring(0, 7)} to the ${event.deployment?.env} environment of ${event.deployment?.repo?.namespace}/${event.deployment?.repo?.name}.`,
tag: String(event.id),
})
return
}

notify(`Deployment Updated #${event.deployment?.number}`, {
icon: "/logo192.png",
body: `The deployment ${event.deployment?.number} of ${event.deployment?.repo?.namespace}/${event.deployment?.repo?.name} is updated ${event.deployment?.status}.`,
tag: String(event.id),
})
}
)

/**
* The browser notifies the requester when the review is responded to,
* but it should notify the reviewer when the review is requested.
*/
export const notifyReviewmentEvent = createAsyncThunk<void, Event, { state: { main: MainState } }>(
"main/notifyReviewmentEvent",
async (event, { getState }) => {
const { user } = getState().main
if (event.kind !== EventKindEnum.Review) {
return
}

if (event.type === EventTypeEnum.Created
&& event.review?.user?.id === user?.id) {
notify(`Review Requested`, {
icon: "/logo192.png",
body: `${event.review?.deployment?.deployer?.login} requested the review for the deployment ${event.review?.deployment?.number} of ${event.review?.deployment?.repo?.namespace}/${event.review?.deployment?.repo?.name}`,
tag: String(event.id),
})
return
}

if (event.type === EventTypeEnum.Updated
&& event.review?.deployment?.deployer?.id === user?.id) {
notify(`Review Responded`, {
icon: "/logo192.png",
body: `${event.review?.user?.login} ${event.review?.status} the deployment ${event.review?.deployment?.number} of ${event.review?.deployment?.repo?.namespace}/${event.review?.deployment?.repo?.name}`,
tag: String(event.id),
})
return
}
}
)


export const mainSlice = createSlice({
name: "main",
initialState,
Expand All @@ -130,54 +215,48 @@ export const mainSlice = createSlice({
setExpired: (state, action: PayloadAction<boolean>) => {
state.expired = action.payload
},
/**
* Handle all deployment events that the user can access.
* Note that some deployments are triggered by others.
*/
handleDeploymentEvent: (state, action: PayloadAction<Event>) => {
const user = state.user
if (!user) {
throw new Error("Unauthorized user.")
}

const event = action.payload
if (event.kind !== EventKindEnum.Deployment) {
return
}

// Handling the event when the owner is same.
if (event.deployment?.deployer?.id !== user.id) {
Comment on lines -141 to -142
Copy link
Member Author

Choose a reason for hiding this comment

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

Remove the condition to display all deployments even though others are triggered.

if (event.type === EventTypeEnum.Created
&& event.deployment) {
state.deployments.unshift(event.deployment)
return
}

// Update the deployment if it exist.
const idx = state.deployments.findIndex((deployment) => {
return event.deployment?.id === deployment.id
})

if (idx !== -1 ) {
// Remove from the list when the status is not one of 'waiting', 'created', and 'running'.
if (!runningDeploymentStatus.includes(event.deployment.status)) {
if (!(event.deployment?.status === DeploymentStatusEnum.Waiting
|| event.deployment?.status === DeploymentStatusEnum.Created
|| event.deployment?.status === DeploymentStatusEnum.Running)) {
state.deployments.splice(idx, 1)
return
}

state.deployments[idx] = event.deployment
return
}

state.deployments.unshift(event.deployment)
},
handleReviewEvent: (state, action: PayloadAction<Event>) => {
const event = action.payload

if (action.payload.kind !== EventKindEnum.Review) {
return
}
}

const user = state.user
if (!user) {
throw new Error("Unauthorized user.")
}

// Handling the event when the user own the event.
if (event.review?.user?.id !== user.id) {
return
}

if (event.type === EventTypeEnum.Created) {
if (event.type === EventTypeEnum.Created
&& event.review
&& event.review?.user?.id === state.user?.id) {
state.reviews.unshift(event.review)
return
}
Expand Down
4 changes: 3 additions & 1 deletion ui/src/views/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { SettingFilled } from "@ant-design/icons"
import { Helmet } from "react-helmet"

import { useAppSelector, useAppDispatch } from "../redux/hooks"
import { init, searchDeployments, searchReviews, fetchLicense, mainSlice as slice } from "../redux/main"
import { init, searchDeployments, searchReviews, fetchLicense, notifyDeploymentEvent, notifyReviewmentEvent, mainSlice as slice } from "../redux/main"
import { subscribeEvents } from "../apis"

import RecentActivities from "../components/RecentActivities"
Expand Down Expand Up @@ -37,6 +37,8 @@ export default function Main(props: any) {
const sub = subscribeEvents((event) => {
dispatch(slice.actions.handleDeploymentEvent(event))
dispatch(slice.actions.handleReviewEvent(event))
dispatch(notifyDeploymentEvent(event))
dispatch(notifyReviewmentEvent(event))
})

return () => {
Expand Down