Skip to content

Commit

Permalink
feat(websocket): pluging websocket and storing result in slices
Browse files Browse the repository at this point in the history
  • Loading branch information
bdebon committed Jun 8, 2022
1 parent 4545fda commit 175a72b
Show file tree
Hide file tree
Showing 36 changed files with 4,483 additions and 4,025 deletions.
2 changes: 1 addition & 1 deletion __tests__/utils/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { initialRootState, rootReducer } from '../../libs/store/data/src'
import { Auth0Provider } from '@auth0/auth0-react'
import { IntercomProvider } from 'react-use-intercom'
import posthog from 'posthog-js'
import { RootState } from '@console/shared/interfaces'
import { RootState } from '@console/store/data'

type Params = {
Component: ComponentType<any>
Expand Down
42 changes: 40 additions & 2 deletions libs/domains/application/src/lib/slices/applications.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {
Link,
Status,
} from 'qovery-typescript-axios'
import { addOneToManyRelation, getEntitiesByIds, removeOneToManyRelation } from '@console/shared/utils'
import { ApplicationEntity, ApplicationsState, LoadingStatus } from '@console/shared/interfaces'
import { addOneToManyRelation, getEntitiesByIds, removeOneToManyRelation, shortToLongId } from '@console/shared/utils'
import { ApplicationEntity, ApplicationsState, LoadingStatus, ServiceRunningStatus } from '@console/shared/interfaces'
import { RootState } from '@console/store/data'

export const APPLICATIONS_FEATURE_KEY = 'applications'
Expand Down Expand Up @@ -101,6 +101,44 @@ export const applicationsSlice = createSlice({
reducers: {
add: applicationsAdapter.addOne,
remove: applicationsAdapter.removeOne,
updateApplicationsRunningStatus: (
state,
action: PayloadAction<{ servicesRunningStatus: ServiceRunningStatus[]; listEnvironmentIdFromCluster: string[] }>
) => {
// we have to force this reset change because of the way the socket works.
// You can have information about an application (eg. if it's stopping)
// But you can also lose the information about this application (eg. it it's stopped it won't appear in the socket result)
const resetChanges: Update<ApplicationEntity>[] = state.ids.map((id) => {
// as we can have this dispatch from different websocket, we don't want to reset
// and override all the application but only the ones associated to the cluster the websocket is
// coming from, more generally from all the environments that are contained in this cluster
const envId = state.entities[id]?.environment?.id

const runningStatusChanges =
envId && action.payload.listEnvironmentIdFromCluster.includes(envId)
? undefined
: state.entities[id]?.running_status
return {
id,
changes: {
running_status: runningStatusChanges,
},
}
})
applicationsAdapter.updateMany(state, resetChanges)

const changes: Update<ApplicationEntity>[] = action.payload.servicesRunningStatus.map((runningStatus) => {
const realId = shortToLongId(runningStatus.id, state.ids as string[])
return {
id: realId,
changes: {
running_status: runningStatus,
},
}
})

applicationsAdapter.updateMany(state, changes)
},
},
extraReducers: (builder) => {
builder
Expand Down
50 changes: 48 additions & 2 deletions libs/domains/environment/src/lib/slices/environments.slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
PayloadAction,
Update,
} from '@reduxjs/toolkit'
import { EnvironmentEntity, EnvironmentsState } from '@console/shared/interfaces'
import { EnvironmentEntity, EnvironmentsState, WebsocketRunningStatusInterface } from '@console/shared/interfaces'
import { Environment, EnvironmentsApi, Status } from 'qovery-typescript-axios'
import { addOneToManyRelation, getEntitiesByIds } from '@console/shared/utils'
import { addOneToManyRelation, getEntitiesByIds, shortToLongId } from '@console/shared/utils'
import { RootState } from '@console/store/data'

export const ENVIRONMENTS_FEATURE_KEY = 'environments'
Expand Down Expand Up @@ -51,6 +51,40 @@ export const environmentsSlice = createSlice({
reducers: {
add: environmentsAdapter.addOne,
remove: environmentsAdapter.removeOne,
updateEnvironmentsRunningStatus: (
state,
action: PayloadAction<{ websocketRunningStatus: WebsocketRunningStatusInterface[]; clusterId: string }>
) => {
// we have to force this reset change because of the way the socket works.
// You can have information about an application (eg. if it's stopping)
// But you can also lose the information about this application (eg. it it's stopped it won't appear in the socket result)
const resetChanges: Update<EnvironmentEntity>[] = state.ids.map((id) => {
// as we can have this dispatch from different websocket, we don't want to reset
// and override all the entry but only the one associated to the cluster the websocket is
// coming from
const runningStatusChanges =
state.entities[id]?.cluster_id === action.payload.clusterId ? undefined : state.entities[id]?.running_status
return {
id,
changes: {
running_status: runningStatusChanges,
},
}
})
environmentsAdapter.updateMany(state, resetChanges)

const changes: Update<EnvironmentEntity>[] = action.payload.websocketRunningStatus.map((runningStatus) => {
const realId = shortToLongId(runningStatus.id, state.ids as string[])
return {
id: realId,
changes: {
running_status: runningStatus,
},
}
})

environmentsAdapter.updateMany(state, changes)
},
},
extraReducers: (builder) => {
builder
Expand Down Expand Up @@ -113,6 +147,18 @@ export const selectEnvironmentsEntitiesByProjectId = (state: RootState, projectI
return getEntitiesByIds<Environment>(environmentState.entities, environmentState?.joinProjectEnvironments[projectId])
}

export const selectEnvironmentsEntitiesByClusterId = (clusterId: string) =>
createSelector(
(state: RootState) => {
return selectAll(getEnvironmentsState(state))
},
(environments): EnvironmentEntity[] => {
return environments.filter((env) => {
return env.cluster_id === clusterId
})
}
)

export const selectEnvironmentById = (state: RootState, environmentId: string) =>
getEnvironmentsState(state).entities[environmentId]

Expand Down
12 changes: 11 additions & 1 deletion libs/domains/organization/src/lib/slices/cluster.slice.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ClustersState } from '@console/shared/interfaces'
import { RootState } from '@console/store/data'
import { createAsyncThunk, createEntityAdapter, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'
import { createAsyncThunk, createEntityAdapter, createSelector, createSlice } from '@reduxjs/toolkit'
import { Cluster, ClustersApi } from 'qovery-typescript-axios'
import { addOneToManyRelation, getEntitiesByIds } from '@console/shared/utils'

Expand Down Expand Up @@ -64,4 +64,14 @@ export const selectClustersEntitiesByOrganizationId = (state: RootState, organiz
return getEntitiesByIds<Cluster>(clusterState.entities, clusterState?.joinOrganizationClusters[organizationId])
}

// export const selectClustersEntitiesByOrganizationIdMemoized = (organizationId: string) =>
// createSelector(
// (state: RootState) => {
// return getClusterState(state)
// },
// (clusterState): Cluster[] => {
// return getEntitiesByIds<Cluster>(clusterState.entities, clusterState?.joinOrganizationClusters[organizationId])
// }
// )

export const selectClusterEntities = createSelector(getClusterState, selectEntities)
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export function Container(props: ContainerProps) {
return (
<>
<Header
title={application?.name}
title={application?.name + ' ' + application?.running_status?.state}
icon={IconEnum.APPLICATION}
buttons={headerButtons}
copyTitle
Expand Down
4 changes: 2 additions & 2 deletions libs/pages/services/feature/src/lib/services-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { Application, Database, Environment } from 'qovery-typescript-axios'
import { SERVICES_URL, APPLICATION_GENERAL_URL, useDocumentTitle } from '@console/shared/utils'
import { Container } from '@console/pages/services/ui'
import { selectApplicationsEntitiesByEnvId } from '@console/domains/application'
import { selectEnvironmentById } from '@console/domains/environment'
import { AppDispatch, RootState } from '@console/store/data'
import {
deleteEnvironmentActionsCancelDeployment,
postEnvironmentActionsCancelDeployment,
postEnvironmentActionsDeploy,
postEnvironmentActionsRestart,
postEnvironmentActionsStop,
selectEnvironmentById,
} from '@console/domains/environment'
import { AppDispatch, RootState } from '@console/store/data'
import { ROUTER_SERVICES } from './router/router'
import { useEffect } from 'react'

Expand Down
1 change: 1 addition & 0 deletions libs/shared/enums/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './lib/colors.enum'
export * from './lib/icon.enum'
export * from './lib/running-status.enum'
10 changes: 10 additions & 0 deletions libs/shared/enums/src/lib/running-status.enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export enum RunningStatus {
STARTING = 'STARTING',
RUNNING = 'RUNNING',
WARNING = 'WARNING',
ERROR = 'ERROR',
STOPPING = 'STOPPING',
STOPPED = 'STOPPED',
TERMINATED = 'TERMINATED',
UNKNOWN = 'UNKNOWN',
}
3 changes: 3 additions & 0 deletions libs/shared/interfaces/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './lib/common/value.interface'
export * from './lib/common/websocket-running-status.interface'
export * from './lib/states/user-sign-up.interface'
export * from './lib/states/environments.interface'
export * from './lib/states/projects.interface'
Expand All @@ -13,4 +14,6 @@ export * from './lib/domain/application.entity'
export * from './lib/domain/database.entity'
export * from './lib/domain/environment.entity'

export * from './lib/domain/service-running-status.interface'

export * from './lib/types/loading-status.type'
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ServiceRunningStatus } from '../domain/service-running-status.interface'
import { RunningStatus } from '@console/shared/enums'

export interface WebsocketRunningStatusInterface {
applications?: ServiceRunningStatus[]
database?: ServiceRunningStatus[]
id: string
project_id: string
state: RunningStatus
}
2 changes: 2 additions & 0 deletions libs/shared/interfaces/src/lib/domain/application.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Application, Commit, Instance, Link, Status } from 'qovery-typescript-axios'
import { LoadingStatus } from '../types/loading-status.type'
import { ServiceRunningStatus } from './service-running-status.interface'

export interface ApplicationEntity extends Application {
status?: Status
Expand All @@ -15,4 +16,5 @@ export interface ApplicationEntity extends Application {
loadingStatus: LoadingStatus
items?: Commit[]
}
running_status?: ServiceRunningStatus
}
2 changes: 2 additions & 0 deletions libs/shared/interfaces/src/lib/domain/environment.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Environment, Status } from 'qovery-typescript-axios'
import { WebsocketRunningStatusInterface } from '../common/websocket-running-status.interface'

export interface EnvironmentEntity extends Environment {
status?: Status
running_status?: WebsocketRunningStatusInterface
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { RunningStatus } from '@console/shared/enums'

export interface ServiceRunningStatus {
id: string
state: RunningStatus
pods: {
name: string
state: RunningStatus
restart_count: 0
state_message: string
}[]
}
12 changes: 10 additions & 2 deletions libs/shared/layout/src/lib/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect } from 'react'
import { useParams } from 'react-router'
import { useOrganization } from '@console/domains/organization'
import { fetchClusters, useOrganization } from '@console/domains/organization'
import { selectProjectsEntitiesByOrgId, useEnvironments, useProjects } from '@console/domains/projects'
import { useUser } from '@console/domains/user'
import { selectApplicationById, useApplication, useApplications } from '@console/domains/application'
Expand All @@ -10,6 +10,7 @@ import { useDispatch, useSelector } from 'react-redux'
import { selectEnvironmentsEntitiesByProjectId } from '@console/domains/environment'
import { Application, Environment, Project } from 'qovery-typescript-axios'
import { AppDispatch, RootState } from '@console/store/data'
import { WebsocketContainer } from '@console/shared/websockets'
import { fetchDatabases, selectAllDatabases } from '@console/domains/database'

export interface LayoutProps {
Expand Down Expand Up @@ -57,6 +58,10 @@ export function Layout(props: LayoutProps) {
dispatch,
])

useEffect(() => {
dispatch(fetchClusters({ organizationId }))
}, [organizationId])

return (
<LayoutPage
authLogout={authLogout}
Expand All @@ -68,7 +73,10 @@ export function Layout(props: LayoutProps) {
application={application}
databases={databases}
>
{children}
<>
<WebsocketContainer />
{children}
</>
</LayoutPage>
)
}
Expand Down
2 changes: 2 additions & 0 deletions libs/shared/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ export * from './lib/http/use-auth/auth.enum'
// hooks
export * from './lib/hooks/use-document-title/use-document-title'
export * from './lib/hooks/use-detect-click-outside/use-detect-click-outside'
export * from './lib/hooks/use-running-status-websocket/use-running-status-websocket'
// tools
export * from './lib/tools/uppercase-first-letter'
export * from './lib/tools/date'
export * from './lib/tools/status-actions-available'
export * from './lib/tools/one-to-many-redux-helpers'
export * from './lib/tools/date-to-hours'
export * from './lib/tools/short-to-long-id'
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { renderHook } from '@testing-library/react-hooks'
import useRunningStatusWebsocket from './use-running-status-websocket'

describe('Running Status Websocket Provider', () => {
it('should render successfully', () => {
const { result } = renderHook(() => useRunningStatusWebsocket())

expect(result).toBeTruthy()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useState } from 'react'
import useAuth from '../../http/use-auth/use-auth'

export interface RunningStatusWebsocketProps {
organizationId: string
}

const baseUrl = 'wss://ws.qovery.com/service/status'

export function useRunningStatusWebsocket() {
const [websockets, setWebsockets] = useState<string[]>([])
const [websocketsUrl, setWebsocketsUrl] = useState<string[]>([])
const { getAccessTokenSilently } = useAuth()

const closeSockets = (): void => {
setWebsockets([])
setWebsocketsUrl([])
}

const openWebSockets = async (organizationId: string, clusterIds: string[]): Promise<string[]> => {
clusterIds.forEach((clusterId) => {
openWebSocket(organizationId, clusterId)
})

return []
}

const openWebSocket = async (organizationId: string, clusterId: string): Promise<void> => {
const token = await getAccessTokenSilently()

setWebsockets((prevValue) => {
const webSocketId = `${organizationId}-${clusterId}`
if (prevValue.indexOf(webSocketId) === -1) return [...prevValue, webSocketId]
return prevValue
})
setWebsocketsUrl((prevValue) => {
const webSocketUrl = `${baseUrl}?organization=${organizationId}&cluster=${clusterId}&bearer_token=${token}`
if (prevValue.indexOf(webSocketUrl) === -1) return [...prevValue, webSocketUrl]
return prevValue
})
}

return {
websockets,
websocketsUrl,
closeSockets,
openWebSockets,
}
}

export default useRunningStatusWebsocket
11 changes: 11 additions & 0 deletions libs/shared/utils/src/lib/tools/short-to-long-id.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { shortToLongId } from './short-to-long-id'

describe('finding the long id from the short id', () => {
const listIds = ['f9613eb4-1bfa-4b74-85bc-864e7fb72abd', '61d95abd-b777-4f53-b832-58c7e7f6f1bf']

it('should use the short id to find the associated long id in the list', () => {
const shortId = 'z61d95abd'
const foundId = shortToLongId(shortId, listIds)
expect(foundId).toEqual('61d95abd-b777-4f53-b832-58c7e7f6f1bf')
})
})
6 changes: 6 additions & 0 deletions libs/shared/utils/src/lib/tools/short-to-long-id.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const shortToLongId = (id: string, listIds: string[]): string => {
if (id[0] !== 'z') return id
id = id.slice(1)

return listIds.find((theId) => theId.indexOf(id) >= 0) || id
}

0 comments on commit 175a72b

Please sign in to comment.