Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show msm connection info #5458

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
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
Loading