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;
});
},