Skip to content

Commit

Permalink
feat: show msm connection info (#5458)
Browse files Browse the repository at this point in the history
- update strip-literal to 2.1.0 - fixes vitest-dev/vitest#5387 (comment)
- add rerender with state updates in renderWithMockStore using immer `produce`.
- add `immer` as a dev dependency
  • Loading branch information
petermakowski authored Jun 12, 2024
1 parent 914c7f0 commit bbe685d
Show file tree
Hide file tree
Showing 16 changed files with 220 additions and 51 deletions.
6 changes: 6 additions & 0 deletions src/__snapshots__/root-reducer.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ exports[`rootReducer > should reset app to initial state on LOGOUT_SUCCESS, exce
"message": {
"items": [],
},
"msm": {
"errors": null,
"loaded": false,
"loading": false,
"status": null,
},
"nodedevice": {
"errors": null,
"items": [],
Expand Down
28 changes: 28 additions & 0 deletions src/app/base/components/StatusBar/StatusBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -248,3 +248,31 @@ it("hides the feedback link in development environment", () => {
screen.queryByRole("button", { name: "Give feedback" })
).not.toBeInTheDocument();
});

it("displays the status message when connected to MAAS Site Manager", () => {
state.msm = factory.msmState({
status: factory.msmStatus({
running: "not_connected",
}),
});

const { rerender } = renderWithMockStore(<StatusBar />, { state });

expect(
screen.queryByText("Connected to MAAS Site Manager")
).not.toBeInTheDocument();

rerender(<StatusBar />, {
state: (draft) => {
draft.msm = factory.msmState({
status: factory.msmStatus({
running: "connected",
}),
});
},
});

expect(
screen.getByText("Connected to MAAS Site Manager")
).toBeInTheDocument();
});
27 changes: 24 additions & 3 deletions src/app/base/components/StatusBar/StatusBar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { ReactNode } from "react";
import { type ReactNode } from "react";

import { Button, Link } from "@canonical/react-components";
import { Button, Icon, Link } from "@canonical/react-components";
import { useSelector } from "react-redux";

import { useUsabilla } from "@/app/base/hooks";
import TooltipButton from "../TooltipButton";

import { useFetchActions, useUsabilla } from "@/app/base/hooks";
import configSelectors from "@/app/store/config/selectors";
import controllerSelectors from "@/app/store/controller/selectors";
import {
Expand All @@ -18,6 +20,8 @@ import {
isDeployedWithHardwareSync,
isMachineDetails,
} from "@/app/store/machine/utils";
import { msmActions } from "@/app/store/msm";
import msmSelectors from "@/app/store/msm/selectors";
import type { UtcDatetime } from "@/app/store/types/model";
import { NodeStatus } from "@/app/store/types/node";
import { formatUtcDatetime, getTimeDistanceString } from "@/app/utils/time";
Expand Down Expand Up @@ -57,6 +61,9 @@ export const StatusBar = (): JSX.Element | null => {
const version = useSelector(versionSelectors.get);
const maasName = useSelector(configSelectors.maasName);
const allowUsabilla = useUsabilla();
const msmRunning = useSelector(msmSelectors.running);

useFetchActions([msmActions.fetch]);

if (!(maasName && version)) {
return null;
Expand Down Expand Up @@ -101,6 +108,20 @@ export const StatusBar = (): JSX.Element | null => {
:&nbsp;
<span data-testid="status-bar-version">{version}</span>
</div>
<div className="p-status-bar__primary u-flex--no-shrink u-flex--wrap">
<span data-testid="status-bar-msm-status">
{msmRunning === "connected" ? (
<TooltipButton
message="This MAAS is connected to a MAAS Site Manager.
It will regularly report to the Site Manager and choose
Site Manager as its upstream image source."
>
<Icon name="connected" />
Connected to MAAS Site Manager
</TooltipButton>
) : null}
</span>
</div>
<ul className="p-inline-list--middot u-no-margin--bottom">
<li className="p-inline-list__item">
<Link
Expand Down
1 change: 1 addition & 0 deletions src/app/store/msm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default, msmActions } from "./slice";
17 changes: 17 additions & 0 deletions src/app/store/msm/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { createSelector } from "@reduxjs/toolkit";

import type { RootState } from "@/app/store/root/types";

const status = (state: RootState) => state.msm.status;
const running = createSelector(status, (status) => status?.running);
const loading = (state: RootState) => state.msm.loading;
const errors = (state: RootState) => state.msm.errors;

const msmSelectors = {
status,
running,
loading,
errors,
};

export default msmSelectors;
42 changes: 42 additions & 0 deletions src/app/store/msm/slice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { PayloadAction } from "@reduxjs/toolkit";
import { createSlice } from "@reduxjs/toolkit";

import type { MsmState, MsmStatus } from "./types/base";

const initialState: MsmState = {
status: null,
errors: null,
loading: false,
loaded: false,
};

const msmSlice = createSlice({
name: "msm",
initialState,
reducers: {
fetch: {
prepare: () => ({
meta: {
model: "msm",
method: "status",
},
payload: null,
}),
reducer: () => {},
},
fetchSuccess(state, action: PayloadAction<MsmStatus>) {
state.status = action.payload;
state.loading = false;
state.loaded = true;
state.errors = null;
},
fetchError(state, action: PayloadAction<string>) {
state.errors = action.payload;
state.loading = false;
},
},
});

export const { actions: msmActions } = msmSlice;

export default msmSlice.reducer;
12 changes: 12 additions & 0 deletions src/app/store/msm/types/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface MsmStatus {
smUrl: string | null;
running: "not_connected" | "pending" | "connected";
startTime: string | null;
}

export interface MsmState {
status: MsmStatus | null;
loading: boolean;
loaded: boolean;
errors: string | null;
}
3 changes: 3 additions & 0 deletions src/app/store/msm/types/enum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export enum MsmMeta {
MODEL = "msm",
}
13 changes: 9 additions & 4 deletions src/app/store/root/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import type { RouterState } from "redux-first-history";

import type { ReservedIpState } from "../reservedip/types";
import type { ReservedIpMeta } from "../reservedip/types/enum";
import type { VMClusterMeta, VMClusterState } from "../vmcluster/types";

import type {
BootResourceState,
BootResourceMeta,
Expand Down Expand Up @@ -33,6 +29,8 @@ import type {
} from "@/app/store/licensekeys/types";
import type { MachineState, MachineMeta } from "@/app/store/machine/types";
import type { MessageState, MessageMeta } from "@/app/store/message/types";
import type { MsmState } from "@/app/store/msm/types/base";
import type { MsmMeta } from "@/app/store/msm/types/enum";
import type {
NodeDeviceState,
NodeDeviceMeta,
Expand All @@ -50,6 +48,8 @@ import type {
PackageRepositoryMeta,
} from "@/app/store/packagerepository/types";
import type { PodState, PodMeta } from "@/app/store/pod/types";
import type { ReservedIpState } from "@/app/store/reservedip/types";
import type { ReservedIpMeta } from "@/app/store/reservedip/types/enum";
import type {
ResourcePoolState,
ResourcePoolMeta,
Expand All @@ -73,6 +73,10 @@ import type { TagState, TagMeta } from "@/app/store/tag/types";
import type { TokenState, TokenMeta } from "@/app/store/token/types";
import type { UserState, UserMeta } from "@/app/store/user/types";
import type { VLANState, VLANMeta } from "@/app/store/vlan/types";
import type {
VMClusterMeta,
VMClusterState,
} from "@/app/store/vmcluster/types";
import type { ZoneState, ZoneMeta } from "@/app/store/zone/types";

export type RootState = {
Expand All @@ -90,6 +94,7 @@ export type RootState = {
[LicenseKeysMeta.MODEL]: LicenseKeysState;
[MachineMeta.MODEL]: MachineState;
[MessageMeta.MODEL]: MessageState;
[MsmMeta.MODEL]: MsmState;
[NodeDeviceMeta.MODEL]: NodeDeviceState;
[NodeScriptResultMeta.MODEL]: NodeScriptResultState;
[NotificationMeta.MODEL]: NotificationState;
Expand Down
2 changes: 2 additions & 0 deletions src/app/store/utils/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { DeviceMeta, DeviceStatus } from "@/app/store/device/types";
import type { GeneralMeta } from "@/app/store/general/types";
import type { MachineMeta, MachineStatus } from "@/app/store/machine/types";
import type { MessageMeta } from "@/app/store/message/types";
import type { MsmMeta } from "@/app/store/msm/types/enum";
import type { NodeScriptResultMeta } from "@/app/store/nodescriptresult/types";
import type { PodMeta, PodStatus } from "@/app/store/pod/types";
import type { RootState } from "@/app/store/root/types";
Expand Down Expand Up @@ -50,6 +51,7 @@ export type CommonStates = Omit<
| ConfigMeta.MODEL
| GeneralMeta.MODEL
| MessageMeta.MODEL
| MsmMeta.MODEL
| NodeScriptResultMeta.MODEL
| StatusMeta.MODEL
| ZoneMeta.MODEL
Expand Down
2 changes: 2 additions & 0 deletions src/root-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import iprange from "@/app/store/iprange";
import licensekeys from "@/app/store/licensekeys";
import machine from "@/app/store/machine";
import message from "@/app/store/message";
import msm from "@/app/store/msm";
import nodedevice from "@/app/store/nodedevice";
import nodescriptresult from "@/app/store/nodescriptresult";
import notification from "@/app/store/notification";
Expand Down Expand Up @@ -61,6 +62,7 @@ const createAppReducer = (routerReducer: Reducer<RouterState, AnyAction>) =>
licensekeys,
machine,
message,
msm,
nodedevice,
nodescriptresult,
notification,
Expand Down
2 changes: 2 additions & 0 deletions src/testing/factories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export {
machineStatus,
machineStatuses,
messageState,
msmState,
msmStatus,
nodeDeviceState,
nodeScriptResultState,
notificationState,
Expand Down
18 changes: 18 additions & 0 deletions src/testing/factories/msm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { define } from "cooky-cutter";

import { timestamp } from "./general";

import type { MsmState, MsmStatus } from "@/app/store/msm/types/base";

const msmStatus = define<MsmStatus | null>({
smUrl: "https://example.com",
running: "not_connected",
startTime: timestamp("Wed, 08 Jul. 2022 05:35:45"),
});

export const msm = define<MsmState>({
status: msmStatus,
loading: false,
loaded: false,
errors: null,
});
14 changes: 14 additions & 0 deletions src/testing/factories/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import type {
} from "@/app/store/machine/types";
import { FilterGroupKey, FilterGroupType } from "@/app/store/machine/types";
import type { MessageState } from "@/app/store/message/types";
import type { MsmState, MsmStatus } from "@/app/store/msm/types/base";
import type { NodeDeviceState } from "@/app/store/nodedevice/types";
import type { NodeScriptResultState } from "@/app/store/nodescriptresult/types";
import type { NotificationState } from "@/app/store/notification/types";
Expand Down Expand Up @@ -424,6 +425,18 @@ export const messageState = define<MessageState>({
items: () => [],
});

export const msmStatus = define<MsmStatus | null>({
running: "not_connected",
smUrl: "http://example.com",
startTime: "2021-01-01",
});
export const msmState = define<MsmState>({
status: msmStatus,
loading: false,
loaded: false,
errors: null,
});

export const architecturesState = define<ArchitecturesState>({
...defaultGeneralState,
});
Expand Down Expand Up @@ -681,6 +694,7 @@ export const rootState = define<RootState>({
licensekeys: licenseKeysState,
machine: machineState,
message: messageState,
msm: msmState,
nodedevice: nodeDeviceState,
notification: notificationState,
nodescriptresult: nodeScriptResultState,
Expand Down
34 changes: 26 additions & 8 deletions src/testing/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { ValueOf } from "@canonical/react-components";
import type { RenderOptions, RenderResult } from "@testing-library/react";
import { render, screen, renderHook } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { produce } from "immer";
import { Provider } from "react-redux";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import type { MockStoreEnhanced } from "redux-mock-store";
Expand Down Expand Up @@ -175,24 +176,41 @@ export const renderWithBrowserRouter = (
};
};

interface WithStoreRenderOptions extends RenderOptions {
state?: RootState | ((stateDraft: RootState) => void);
store?: WrapperProps["store"];
}

export const renderWithMockStore = (
ui: React.ReactNode,
options?: RenderOptions & {
state?: RootState;
store?: WrapperProps["store"];
}
): RenderResult => {
options?: WithStoreRenderOptions
): Omit<RenderResult, "rerender"> & {
rerender: (ui: React.ReactNode, newOptions?: WithStoreRenderOptions) => void;
} => {
const { state, store, ...renderOptions } = options ?? {};
const initialState =
typeof state === "function"
? produce(rootStateFactory(), state)
: state || rootStateFactory();

const rendered = render(ui, {
wrapper: (props) => (
<WithMockStoreProvider {...props} state={state} store={store} />
<WithMockStoreProvider {...props} state={initialState} store={store} />
),
...renderOptions,
});
return {
...rendered,
rerender: (ui: React.ReactNode) =>
renderWithMockStore(ui, { container: rendered.container, ...options }),
rerender: (ui: React.ReactNode, newOptions?: WithStoreRenderOptions) =>
renderWithMockStore(ui, {
container: rendered.container,
...options,
...newOptions,
state:
state && typeof newOptions?.state === "function"
? produce(state, newOptions.state)
: newOptions?.state || state,
}),
};
};

Expand Down
Loading

0 comments on commit bbe685d

Please sign in to comment.