diff --git a/pkg/app/web/src/components/app-live-state.stories.tsx b/pkg/app/web/src/components/app-live-state.stories.tsx new file mode 100644 index 0000000000..0413cd7fa1 --- /dev/null +++ b/pkg/app/web/src/components/app-live-state.stories.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { Provider } from "react-redux"; +import { createStore } from "../../test-utils"; +import { dummyApplication } from "../__fixtures__/dummy-application"; +import { dummyApplicationLiveState } from "../__fixtures__/dummy-application-live-state"; +import { AppLiveState } from "./app-live-state"; + +export default { + title: "APPLICATION/AppLiveState", + component: AppLiveState, +}; + +export const overview: React.FC = () => ( + + + +); + +export const loading: React.FC = () => ( + + + +); + +export const notAvailable: React.FC = () => ( + + + +); diff --git a/pkg/app/web/src/components/app-live-state.tsx b/pkg/app/web/src/components/app-live-state.tsx new file mode 100644 index 0000000000..44d6497aa0 --- /dev/null +++ b/pkg/app/web/src/components/app-live-state.tsx @@ -0,0 +1,53 @@ +import React, { FC, memo } from "react"; +import { Box, makeStyles, Typography } from "@material-ui/core"; +import { useSelector } from "react-redux"; +import { AppState } from "../modules"; +import { + ApplicationLiveState, + selectById, + selectLoadingById, +} from "../modules/applications-live-state"; +import Skeleton from "@material-ui/lab/Skeleton"; +import { ApplicationHealthStatusIcon } from "./health-status-icon"; +import { APPLICATION_HEALTH_STATUS_TEXT } from "../constants/health-status-text"; +import { UI_TEXT_NOT_AVAILABLE_TEXT } from "../constants/ui-text"; + +const useStyles = makeStyles((theme) => ({ + liveStateText: { + marginLeft: theme.spacing(0.5), + }, +})); + +interface Props { + applicationId: string; +} + +export const AppLiveState: FC = memo(function AppLiveState({ + applicationId, +}) { + const classes = useStyles(); + const [liveState, liveStateLoading] = useSelector< + AppState, + [ApplicationLiveState | undefined, boolean] + >((state) => [ + selectById(state.applicationLiveState, applicationId), + selectLoadingById(state.applicationLiveState, applicationId), + ]); + + if (liveStateLoading) { + return ; + } + + return ( + + {liveState ? ( + + ) : null} + + {liveState + ? APPLICATION_HEALTH_STATUS_TEXT[liveState.healthStatus] + : UI_TEXT_NOT_AVAILABLE_TEXT} + + + ); +}); diff --git a/pkg/app/web/src/components/application-detail.stories.tsx b/pkg/app/web/src/components/application-detail.stories.tsx index b06126a169..013745be8b 100644 --- a/pkg/app/web/src/components/application-detail.stories.tsx +++ b/pkg/app/web/src/components/application-detail.stories.tsx @@ -32,6 +32,7 @@ const dummyStore: Partial = { [dummyApplicationLiveState.applicationId]: dummyApplicationLiveState, }, ids: [dummyApplicationLiveState.applicationId], + loading: {}, hasError: {}, }, pipeds: { @@ -91,6 +92,9 @@ export const loadingLiveState: React.FC = () => ( applicationLiveState: { entities: {}, ids: [], + loading: { + [dummyApplication.id]: true, + }, }, applications: { adding: false, @@ -116,3 +120,30 @@ export const loadingLiveState: React.FC = () => ( ); + +export const notAvailable: React.FC = () => ( + + + +); diff --git a/pkg/app/web/src/components/application-detail.test.tsx b/pkg/app/web/src/components/application-detail.test.tsx index 2c3d40da92..e487cedb14 100644 --- a/pkg/app/web/src/components/application-detail.test.tsx +++ b/pkg/app/web/src/components/application-detail.test.tsx @@ -59,6 +59,7 @@ const baseState: DeepPartial = { entities: { [dummyApplicationLiveState.applicationId]: dummyApplicationLiveState, }, + loading: {}, hasError: {}, }, environments: { diff --git a/pkg/app/web/src/components/application-detail.tsx b/pkg/app/web/src/components/application-detail.tsx index aaa0b89709..c766caec33 100644 --- a/pkg/app/web/src/components/application-detail.tsx +++ b/pkg/app/web/src/components/application-detail.tsx @@ -16,7 +16,6 @@ import React, { FC, memo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { Link as RouterLink } from "react-router-dom"; import { APPLICATION_KIND_TEXT } from "../constants/application-kind"; -import { APPLICATION_HEALTH_STATUS_TEXT } from "../constants/health-status-text"; import { PAGE_PATH_DEPLOYMENTS } from "../constants/path"; import { UI_TEXT_REFRESH } from "../constants/ui-text"; import { AppState } from "../modules"; @@ -27,19 +26,15 @@ import { selectById as selectApplicationById, syncApplication, } from "../modules/applications"; -import { - ApplicationLiveState, - selectById as selectLiveStateById, -} from "../modules/applications-live-state"; import { SyncStrategy } from "../modules/deployments"; import { Environment, selectById as selectEnvById, } from "../modules/environments"; import { Piped, selectById as selectPipeById } from "../modules/pipeds"; +import { AppLiveState } from "./app-live-state"; import { AppSyncStatus } from "./app-sync-status"; import { DetailTableRow } from "./detail-table-row"; -import { ApplicationHealthStatusIcon } from "./health-status-icon"; import { SplitButton } from "./split-button"; import { SyncStateReason } from "./sync-state-reason"; @@ -64,9 +59,6 @@ const useStyles = makeStyles((theme) => ({ appSyncState: { marginRight: theme.spacing(1), }, - liveStateText: { - marginLeft: theme.spacing(0.5), - }, buttonProgress: { color: theme.palette.primary.main, position: "absolute", @@ -157,16 +149,11 @@ export const ApplicationDetail: FC = memo(function ApplicationDetail({ const classes = useStyles(); const dispatch = useDispatch(); - const [app, liveState, fetchApplicationError] = useSelector< + const [app, fetchApplicationError] = useSelector< AppState, - [ - Application | undefined, - ApplicationLiveState | undefined, - SerializedError | null - ] + [Application | undefined, SerializedError | null] >((state) => [ selectApplicationById(state.applications, applicationId), - selectLiveStateById(state.applicationLiveState, applicationId), state.applications.fetchApplicationError, ]); @@ -242,19 +229,7 @@ export const ApplicationDetail: FC = memo(function ApplicationDetail({ size="large" className={classes.appSyncState} /> - - {liveState ? ( - <> - - - {APPLICATION_HEALTH_STATUS_TEXT[liveState.healthStatus]} - - - ) : ( - - )} + {app.syncState && ( diff --git a/pkg/app/web/src/modules/applications-live-state.test.ts b/pkg/app/web/src/modules/applications-live-state.test.ts index 58175b2f88..7f74aa7dc0 100644 --- a/pkg/app/web/src/modules/applications-live-state.test.ts +++ b/pkg/app/web/src/modules/applications-live-state.test.ts @@ -8,6 +8,7 @@ import { const initialState: ApplicationLiveStateState = { entities: {}, hasError: {}, + loading: {}, ids: [], }; @@ -29,7 +30,11 @@ describe("applicationLiveStateSlice reducer", () => { arg: "application-1", }, }) - ).toEqual({ ...initialState, hasError: { "application-1": false } }); + ).toEqual({ + ...initialState, + hasError: { "application-1": false }, + loading: { "application-1": true }, + }); }); it(`should handle ${fetchApplicationStateById.rejected.type}`, () => { @@ -43,7 +48,11 @@ describe("applicationLiveStateSlice reducer", () => { }, } ) - ).toEqual({ ...initialState, hasError: { "application-1": true } }); + ).toEqual({ + ...initialState, + hasError: { "application-1": true }, + loading: { "application-1": false }, + }); }); it(`should handle ${fetchApplicationStateById.fulfilled.type}`, () => { @@ -64,6 +73,7 @@ describe("applicationLiveStateSlice reducer", () => { }, ids: [dummyApplicationLiveState.applicationId], hasError: { "application-1": false }, + loading: { "application-1": false }, }); }); }); diff --git a/pkg/app/web/src/modules/applications-live-state.ts b/pkg/app/web/src/modules/applications-live-state.ts index cc16c4f419..1ecc9e0139 100644 --- a/pkg/app/web/src/modules/applications-live-state.ts +++ b/pkg/app/web/src/modules/applications-live-state.ts @@ -39,8 +39,10 @@ export const fetchApplicationStateById = createAsyncThunk< }); const initialState = applicationLiveStateAdapter.getInitialState<{ + loading: Record; hasError: Record; }>({ + loading: {}, hasError: {}, }); @@ -53,6 +55,13 @@ export const selectHasError = ( return state.hasError[applicationId] || false; }; +export const selectLoadingById = ( + state: ApplicationLiveStateState, + applicationId: string +): boolean => { + return state.loading[applicationId] || false; +}; + export const applicationLiveStateSlice = createSlice({ name: "applicationLiveState", initialState, @@ -60,15 +69,18 @@ export const applicationLiveStateSlice = createSlice({ extraReducers: (builder) => { builder .addCase(fetchApplicationStateById.pending, (state, action) => { + state.loading[action.meta.arg] = true; state.hasError[action.meta.arg] = false; }) .addCase(fetchApplicationStateById.fulfilled, (state, action) => { + state.loading[action.meta.arg] = false; state.hasError[action.meta.arg] = false; if (action.payload) { applicationLiveStateAdapter.upsertOne(state, action.payload); } }) .addCase(fetchApplicationStateById.rejected, (state, action) => { + state.loading[action.meta.arg] = false; state.hasError[action.meta.arg] = true; }); },