From 5e8ef26c4a53e69744b9c8d711d8b7ab32b77765 Mon Sep 17 00:00:00 2001 From: Vladimir Cucu <108150922+vladimir-cucu@users.noreply.github.com> Date: Mon, 15 Jan 2024 11:02:17 +0200 Subject: [PATCH] WD-7718 - feat: Remove additional controllers feature (#1676) --- HACKING.md | 25 -- src/juju/api.test.ts | 65 ++-- src/juju/api.ts | 10 +- .../ControllersIndex.test.tsx | 75 +---- .../ControllersIndex/ControllersIndex.tsx | 58 +--- src/pages/ControllersIndex/_controllers.scss | 13 - src/panels/Panels.test.tsx | 8 - src/panels/Panels.tsx | 3 - .../RegisterController.test.tsx | 178 ----------- .../RegisterController/RegisterController.tsx | 281 ------------------ .../register-controller.scss | 47 --- src/store/app/actions.ts | 27 +- src/store/app/thunks.test.ts | 43 +-- src/store/app/thunks.ts | 33 +- src/store/juju/types.ts | 7 +- src/store/middleware/model-poller.test.ts | 1 - src/store/middleware/model-poller.ts | 9 +- src/testing/factories/juju/juju.ts | 9 - 18 files changed, 39 insertions(+), 853 deletions(-) delete mode 100644 src/panels/RegisterController/RegisterController.test.tsx delete mode 100644 src/panels/RegisterController/RegisterController.tsx delete mode 100644 src/panels/RegisterController/register-controller.scss diff --git a/HACKING.md b/HACKING.md index 52da79ba3..4ac5d303f 100644 --- a/HACKING.md +++ b/HACKING.md @@ -40,7 +40,6 @@ contribute and what kinds of contributions are welcome. - [Deploying a local app](#deploying-a-local-app) - [Setting up cross model integrations](#setting-up-cross-model-integrations) - [Getting models into a broken state](#getting-models-into-a-broken-state) - - [Adding additional controllers](#adding-additional-controllers) ## Setting up the dashboard for development @@ -597,27 +596,3 @@ To get the model out of the broken state run: ```shell juju exec --app nginx "status-set --application=True active" ``` - -### Adding additional controllers - -To add additional controllers in the dashboard you'll need more than one controller. You might -like to [set up two controllers](#juju-controllers-in-multipass), each in their -own Multipass. - -You might also like to add some models and deploy some applications on each controller. - -Once you have your controllers set up, [configure your dashboard](#controller-configuration) to point to one -of the controllers. - -At this point you should probably [accept the -certificates](#self-signed-certificates) for both controllers. - -Load the dashboard and go to the controllers list (click Controllers in the -sidebar). - -Now click the 'Register a controller' button. Fill in the form using the details -of the second controller (you can get the IP address of the controller using -`multipass list`) and then click 'Add Controller'. - -The second controller should now be visible in the controller list and going to -the model list should display the models from both controllers. diff --git a/src/juju/api.test.ts b/src/juju/api.test.ts index c39647227..e43a3a147 100644 --- a/src/juju/api.test.ts +++ b/src/juju/api.test.ts @@ -25,6 +25,7 @@ import { } from "testing/factories/juju/ModelManagerV9"; import { controllerFactory, + controllerInfoFactory, modelListInfoFactory, } from "testing/factories/juju/juju"; import { connectionInfoFactory } from "testing/factories/juju/jujulib"; @@ -722,12 +723,8 @@ describe("Juju API", () => { jimM: { listControllers: jest.fn().mockResolvedValueOnce({ controllers: [ - { - "agent-version": "1.2.3", - name: "jaas1", - uuid: "abc123", - }, - { path: "admin/jaas2", uuid: "def456" }, + controllerInfoFactory.build({ name: "jaas1" }), + controllerInfoFactory.build({ name: "jaas2" }), ], }), }, @@ -737,30 +734,21 @@ describe("Juju API", () => { .spyOn(jujuLibVersions, "jujuUpdateAvailable") .mockImplementationOnce(async () => true) .mockImplementationOnce(async () => false); - await fetchControllerList( - "wss://example.com/api", - conn, - true, - dispatch, - () => rootStateFactory.build(), + await fetchControllerList("wss://example.com/api", conn, dispatch, () => + rootStateFactory.build(), ); expect(dispatch).toHaveBeenCalledWith( jujuActions.updateControllerList({ wsControllerURL: "wss://example.com/api", controllers: [ - { - "agent-version": "1.2.3", + controllerInfoFactory.build({ name: "jaas1", - uuid: "abc123", - additionalController: true, updateAvailable: true, - }, - { - path: "admin/jaas2", - uuid: "def456", - additionalController: true, + }), + controllerInfoFactory.build({ + name: "jaas2", updateAvailable: false, - }, + }), ], }), ); @@ -781,12 +769,8 @@ describe("Juju API", () => { .spyOn(jujuLibVersions, "jujuUpdateAvailable") .mockImplementationOnce(async () => true) .mockImplementationOnce(async () => false); - await fetchControllerList( - "wss://example.com/api", - conn, - true, - dispatch, - () => rootStateFactory.build(), + await fetchControllerList("wss://example.com/api", conn, dispatch, () => + rootStateFactory.build(), ); expect(dispatch).toHaveBeenCalledWith( jujuActions.updateControllerList({ @@ -825,7 +809,6 @@ describe("Juju API", () => { await fetchControllerList( "wss://example.com/api", conn, - true, dispatch, () => state, ); @@ -836,7 +819,6 @@ describe("Juju API", () => { { path: "admin/juju1", uuid: "abc123", - additionalController: true, updateAvailable: true, version: "1.2.3", }, @@ -851,13 +833,7 @@ describe("Juju API", () => { facades: { jimM: { listControllers: jest.fn().mockResolvedValueOnce({ - controllers: [ - { - "agent-version": "1.2.3", - name: "jaas1", - uuid: "abc123", - }, - ], + controllers: [controllerInfoFactory.build({ name: "jaas1" })], }), }, }, @@ -865,24 +841,17 @@ describe("Juju API", () => { jest .spyOn(jujuLibVersions, "jujuUpdateAvailable") .mockImplementationOnce(async () => null); - await fetchControllerList( - "wss://example.com/api", - conn, - true, - dispatch, - () => rootStateFactory.build(), + await fetchControllerList("wss://example.com/api", conn, dispatch, () => + rootStateFactory.build(), ); expect(dispatch).toHaveBeenCalledWith( jujuActions.updateControllerList({ wsControllerURL: "wss://example.com/api", controllers: [ - { - "agent-version": "1.2.3", + controllerInfoFactory.build({ name: "jaas1", - uuid: "abc123", - additionalController: true, updateAvailable: false, - }, + }), ], }), ); diff --git a/src/juju/api.ts b/src/juju/api.ts index b404c411a..853eb3f80 100644 --- a/src/juju/api.ts +++ b/src/juju/api.ts @@ -397,12 +397,10 @@ export async function fetchAllModelStatuses( @param wsControllerURL The URL of the controller. @param conn The Juju controller connection. @param reduxStore The applications reduxStore. - @param additionalController If this is an additional controller. */ export async function fetchControllerList( wsControllerURL: string, conn: ConnectionWithFacades, - additionalController: boolean, dispatch: Store["dispatch"], getState: () => RootState, ) { @@ -410,12 +408,7 @@ export async function fetchControllerList( if (conn.facades.jimM) { try { const response = await conn.facades.jimM?.listControllers(); - controllers = response.controllers - ? response.controllers.map((controller) => ({ - ...controller, - additionalController, - })) - : []; + controllers = response.controllers ?? []; } catch (error) { dispatch( generalActions.storeConnectionError( @@ -435,7 +428,6 @@ export async function fetchControllerList( uuid: controllerConfig.config["controller-uuid"], version: getControllerConnection(getState(), wsControllerURL) ?.serverVersion, - additionalController, }, ]; } diff --git a/src/pages/ControllersIndex/ControllersIndex.test.tsx b/src/pages/ControllersIndex/ControllersIndex.test.tsx index 758be7f41..283e39b6b 100644 --- a/src/pages/ControllersIndex/ControllersIndex.test.tsx +++ b/src/pages/ControllersIndex/ControllersIndex.test.tsx @@ -4,7 +4,6 @@ import userEvent from "@testing-library/user-event"; import type { RootState } from "store/store"; import { generalStateFactory } from "testing/factories/general"; import { - additionalControllerFactory, controllerFactory, jujuStateFactory, modelDataApplicationFactory, @@ -18,7 +17,7 @@ import { connectionInfoFactory } from "testing/factories/juju/jujulib"; import { rootStateFactory } from "testing/factories/root"; import { renderComponent } from "testing/utils"; -import ControllersIndex, { Label } from "./ControllersIndex"; +import ControllersIndex from "./ControllersIndex"; describe("Controllers table", () => { let state: RootState; @@ -45,67 +44,6 @@ describe("Controllers table", () => { expect(screen.getAllByRole("row")).toHaveLength(3); }); - it("displays additional controllers", () => { - state.general.controllerConnections = { - "wss://jimm.jujucharms.com/api": connectionInfoFactory.build({}), - }; - state.juju = jujuStateFactory.build({ - controllers: { - "wss://jimm.jujucharms.com/api": [ - controllerFactory.build(), - additionalControllerFactory.build(), - ], - }, - }); - renderComponent(, { state }); - const tables = screen.getAllByRole("grid"); - expect(tables).toHaveLength(2); - const additionalRows = within(tables[1]).getAllByRole("row"); - expect(additionalRows).toHaveLength(2); - expect(additionalRows[1]).toHaveTextContent( - [ - "jimm.jujucharms.com", - "Connected", - "unknown/unknown", - "0", - "0", - "0", - "0", - ].join(""), - ); - }); - - it("displays additional controllers from JIMM", () => { - state.general.controllerConnections = { - "wss://jimm.jujucharms.com/api": connectionInfoFactory.build(), - }; - state.juju = jujuStateFactory.build({ - controllers: { - "wss://jimm.jujucharms.com/api": [ - controllerFactory.build(), - controllerInfoFactory.build({ additionalController: true }), - ], - }, - }); - renderComponent(, { state }); - const tables = screen.getAllByRole("grid"); - expect(tables).toHaveLength(2); - const additionalRows = within(tables[1]).getAllByRole("row"); - expect(additionalRows).toHaveLength(2); - expect(additionalRows[1]).toHaveTextContent( - [ - "controller1", - "Connected", - "unknown/unknown", - "0", - "0", - "0", - "0", - "1.2.3", - ].join(""), - ); - }); - it("counts models, machines, apps, and units", () => { state.general.controllerConnections = { "wss://jimm.jujucharms.com/api": connectionInfoFactory.build(), @@ -193,17 +131,6 @@ describe("Controllers table", () => { ).toBeInTheDocument(); }); - it("shows 'Register new controller' panel", async () => { - renderComponent(, { url: "/controllers" }); - await userEvent.click( - screen.getByRole("button", { name: Label.REGISTER_BUTTON }), - ); - expect(window.location.search).toBe("?panel=register-controller"); - expect( - document.querySelector(".p-panel.register-controller"), - ).toBeInTheDocument(); - }); - it("Indicates if a controller has an update available", () => { state.juju = jujuStateFactory.build({ controllers: { diff --git a/src/pages/ControllersIndex/ControllersIndex.tsx b/src/pages/ControllersIndex/ControllersIndex.tsx index eb84a539b..4dd997ff5 100644 --- a/src/pages/ControllersIndex/ControllersIndex.tsx +++ b/src/pages/ControllersIndex/ControllersIndex.tsx @@ -8,14 +8,12 @@ import type { MainTableCell, MainTableHeader, } from "@canonical/react-components/dist/components/MainTable/MainTable"; -import cloneDeep from "clone-deep"; import { useSelector } from "react-redux"; import { Link } from "react-router-dom"; import AuthenticationButton from "components/AuthenticationButton"; import Status from "components/Status"; import TruncatedTooltip from "components/TruncatedTooltip"; -import { useQueryParams } from "hooks/useQueryParams"; import useWindowTitle from "hooks/useWindowTitle"; import BaseLayout from "layout/BaseLayout/BaseLayout"; import { @@ -28,7 +26,7 @@ import { getControllerData, getModelData, } from "store/juju/selectors"; -import type { AdditionalController, Controller } from "store/juju/types"; +import type { Controller } from "store/juju/types"; import { useAppSelector } from "store/store"; import urls from "urls"; import { breakLines } from "utils"; @@ -39,10 +37,9 @@ import "./_controllers.scss"; export enum Label { DEFAULT = "Default", NAME = "Name", - REGISTER_BUTTON = "Register a controller", } -type AnnotatedController = (Controller | AdditionalController) & { +type AnnotatedController = Controller & { models: number; machines: number; applications: number; @@ -50,15 +47,6 @@ type AnnotatedController = (Controller | AdditionalController) & { wsControllerURL: string; }; -const generateRegisteredTitle = () => ( - - Registered{" "} - - - - -); - function Details() { useWindowTitle("Controllers"); const controllerConnections = useAppSelector(getControllerConnections); @@ -68,14 +56,10 @@ function Details() { const visitURLs = useAppSelector(getVisitURLs); const controllerMap: Record = {}; - const additionalControllers: string[] = []; if (controllerData) { Object.entries(controllerData).forEach(([wsControllerURL, controllers]) => { controllers.forEach((controller) => { const id = "uuid" in controller ? controller.uuid : wsControllerURL; - if (controller.additionalController) { - additionalControllers.push(id); - } controllerMap[id] = { ...controller, models: 0, @@ -132,9 +116,6 @@ function Details() { { content: "version", sortKey: "version" }, ]; - const additionalHeaders = cloneDeep(headers); - additionalHeaders[0].content = generateRegisteredTitle(); - function generatePathValue(controllerData: AnnotatedController) { const column: MainTableCell = { content: "" }; // Remove protocol and trailing /api from websocket addresses. @@ -254,16 +235,6 @@ function Details() { columns, }; } - // XXX this isn't a great way of doing this. - const additionalRows = additionalControllers.map((uuid) => { - const row = generateRow( - controllerMap[uuid], - !!controllerConnections && - controllerMap[uuid].wsControllerURL in controllerConnections, - ); - delete controllerMap[uuid]; - return row; - }); const rows = controllerMap && @@ -275,10 +246,6 @@ function Details() { ), ); - const [, setPanelQs] = useQueryParams<{ panel: string | null }>({ - panel: null, - }); - return ( <> {visitURLs?.map((visitURL) => ( @@ -294,17 +261,6 @@ function Details() {
Model status across controllers
-
- -
@@ -314,16 +270,6 @@ function Details() { )} - {additionalRows.length > 0 && ( - <> -
{generateRegisteredTitle()}
- - - )}
); diff --git a/src/pages/ControllersIndex/_controllers.scss b/src/pages/ControllersIndex/_controllers.scss index 5a13b3ddc..0192ffb83 100644 --- a/src/pages/ControllersIndex/_controllers.scss +++ b/src/pages/ControllersIndex/_controllers.scss @@ -50,19 +50,6 @@ top: 0; } } - - .controllers--register { - text-align: right; - - @include mobile { - text-align: left; - } - } - - button { - margin: 0; - max-width: 15rem; - } } &__status { diff --git a/src/panels/Panels.test.tsx b/src/panels/Panels.test.tsx index 46491c231..01c88e94e 100644 --- a/src/panels/Panels.test.tsx +++ b/src/panels/Panels.test.tsx @@ -7,17 +7,9 @@ import { TestId as AuditLogsFilterPanelTestId } from "./AuditLogsFilterPanel/Aud import { TestId as CharmsAndActionsPanelTestId } from "./CharmsAndActionsPanel/CharmsAndActionsPanel"; import { TestId as ConfigPanelTestId } from "./ConfigPanel/ConfigPanel"; import Panels from "./Panels"; -import { Label as RegisterControllerLabel } from "./RegisterController/RegisterController"; import { TestId as ShareModelTestId } from "./ShareModelPanel/ShareModel"; describe("Panels", () => { - it("can display the register controller panel", () => { - renderComponent(, { url: "/?panel=register-controller" }); - expect( - screen.getByRole("dialog", { name: RegisterControllerLabel.TITLE }), - ).toBeInTheDocument(); - }); - it("can display the actions panel", () => { renderComponent(, { url: "/?panel=execute-action" }); expect(screen.getByTestId(ActionsPanelTestId.PANEL)).toBeInTheDocument(); diff --git a/src/panels/Panels.tsx b/src/panels/Panels.tsx index 70b080876..25a9f1e67 100644 --- a/src/panels/Panels.tsx +++ b/src/panels/Panels.tsx @@ -2,7 +2,6 @@ import { AnimatePresence } from "framer-motion"; import { useQueryParams } from "hooks/useQueryParams"; import ActionsPanel from "panels/ActionsPanel/ActionsPanel"; -import RegisterController from "panels/RegisterController/RegisterController"; import ShareModel from "panels/ShareModelPanel/ShareModel"; import AuditLogsFilterPanel from "./AuditLogsFilterPanel"; @@ -16,8 +15,6 @@ export default function Panels() { const generatePanel = () => { switch (panelQs.panel) { - case "register-controller": - return ; case "execute-action": return ; case "share-model": diff --git a/src/panels/RegisterController/RegisterController.test.tsx b/src/panels/RegisterController/RegisterController.test.tsx deleted file mode 100644 index b87b92abb..000000000 --- a/src/panels/RegisterController/RegisterController.test.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import { screen, waitFor } from "@testing-library/react"; -import userEvent from "@testing-library/user-event"; - -import { thunks as appThunks } from "store/app"; -import * as dashboardStore from "store/store"; -import { renderComponent } from "testing/utils"; - -import RegisterController, { Label, STORAGE_KEY } from "./RegisterController"; - -describe("RegisterController", () => { - const consoleError = console.error; - - beforeEach(() => { - console.error = jest.fn(); - }); - - afterEach(() => { - localStorage.clear(); - console.error = consoleError; - }); - - it("can register a controller", async () => { - // Mock the result of the thunk to be a normal action so that it can be tested - // for. This is necessary because we don't have a full store set up which - // can dispatch thunks (and we don't need to handle the thunk, just know it - // was dispatched). - jest - .spyOn(appThunks, "connectAndStartPolling") - .mockImplementation( - jest.fn().mockReturnValue({ type: "connectAndStartPolling" }), - ); - jest.spyOn(dashboardStore, "useAppDispatch").mockImplementation( - jest.fn().mockReturnValue((action: unknown) => { - if ( - action instanceof Object && - "type" in action && - action.type === "connectAndStartPolling" - ) { - store.dispatch(appThunks.connectAndStartPolling() as any); - return Promise.resolve({ catch: jest.fn() }); - } - return null; - }), - ); - const { store } = renderComponent(); - await userEvent.type( - screen.getByRole("textbox", { - name: "Name", - }), - "controller1", - ); - await userEvent.type( - screen.getByRole("textbox", { - name: "Host", - }), - "1.2.3.4:567", - ); - await userEvent.type( - screen.getByRole("textbox", { - name: "Username", - }), - "eggman@external", - ); - await userEvent.click( - screen.getByRole("checkbox", { - name: "The SSL certificate, if any, has been accepted. *", - }), - ); - await userEvent.click(screen.getByRole("button", { name: Label.SUBMIT })); - expect(JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "")).toStrictEqual([ - [ - "wss://1.2.3.4:567/api", - { - password: "", - user: "eggman@external", - }, - null, - true, - ], - ]); - expect( - store - .getActions() - .find((action) => action.type === "connectAndStartPolling"), - ).toBeTruthy(); - }); - - it("clears and disables the username and password if external auth is set", async () => { - renderComponent(); - await userEvent.type( - screen.getByRole("textbox", { - name: "Username", - }), - "eggman@external", - ); - await userEvent.type(screen.getByLabelText("Password"), "verysecure123"); - expect(screen.getByRole("textbox", { name: "Username" })).toHaveValue( - "eggman@external", - ); - expect(screen.getByLabelText("Password")).toHaveValue("verysecure123"); - await userEvent.click( - screen.getByRole("checkbox", { - name: "This controller uses an external identity provider.", - }), - ); - expect(screen.getByRole("textbox", { name: "Username" })).toHaveValue(""); - expect(screen.getByRole("textbox", { name: "Username" })).toBeDisabled(); - expect(screen.getByLabelText("Password")).toHaveValue(""); - expect(screen.getByLabelText("Password")).toBeDisabled(); - }); - - it("requires the certificate warning to be checked", async () => { - renderComponent(); - expect(screen.getByRole("button", { name: Label.SUBMIT })).toBeDisabled(); - await userEvent.click( - screen.getByRole("checkbox", { - name: "The SSL certificate, if any, has been accepted. *", - }), - ); - expect( - screen.getByRole("button", { name: Label.SUBMIT }), - ).not.toBeDisabled(); - }); - - it("should show console error when dispatching connectAndStartPolling", async () => { - jest - .spyOn(appThunks, "connectAndStartPolling") - .mockImplementation( - jest.fn().mockReturnValue({ type: "connectAndStartPolling" }), - ); - jest.spyOn(dashboardStore, "useAppDispatch").mockImplementation( - jest.fn().mockReturnValue((action: unknown) => { - if ( - action instanceof Object && - "type" in action && - action.type === "connectAndStartPolling" - ) { - return Promise.reject( - new Error("Error while trying to dispatch connectAndStartPolling!"), - ); - } - return null; - }), - ); - renderComponent(); - await userEvent.type( - screen.getByRole("textbox", { - name: "Name", - }), - "controller1", - ); - await userEvent.type( - screen.getByRole("textbox", { - name: "Host", - }), - "1.2.3.4:567", - ); - await userEvent.type( - screen.getByRole("textbox", { - name: "Username", - }), - "eggman@external", - ); - await userEvent.click( - screen.getByRole("checkbox", { - name: "The SSL certificate, if any, has been accepted. *", - }), - ); - await userEvent.click(screen.getByRole("button", { name: Label.SUBMIT })); - expect(appThunks.connectAndStartPolling).toHaveBeenCalledTimes(1); - await waitFor(() => - expect(console.error).toHaveBeenCalledWith( - Label.POLLING_ERROR, - new Error("Error while trying to dispatch connectAndStartPolling!"), - ), - ); - }); -}); diff --git a/src/panels/RegisterController/RegisterController.tsx b/src/panels/RegisterController/RegisterController.tsx deleted file mode 100644 index 91404d371..000000000 --- a/src/panels/RegisterController/RegisterController.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import { Button, Col, Row } from "@canonical/react-components"; -import type { ChangeEvent } from "react"; -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; - -import Panel from "components/Panel"; -import useLocalStorage from "hooks/useLocalStorage"; -import { usePanelQueryParams } from "panels/hooks"; -import { thunks as appThunks } from "store/app"; -import type { ControllerArgs } from "store/app/actions"; -import { useAppDispatch } from "store/store"; -import urls from "urls"; - -import "./register-controller.scss"; - -export enum Label { - SUBMIT = "Add Controller", - TITLE = "Register a controller", - POLLING_ERROR = "Error while trying to connect and start polling.", -} - -export const STORAGE_KEY = "additionalControllers"; - -type FormValues = { - controllerName?: string; - wsControllerHost?: string; - username: string; - password: string; - identityProvider?: boolean; - certificateAccepted?: boolean; -}; - -type RegisterControllerQueryParams = { - panel: string | null; -}; - -export default function RegisterController() { - const [formValues, setFormValues] = useState({ - password: "", - username: "", - }); - const dispatch = useAppDispatch(); - const [additionalControllers, setAdditionalControllers] = useLocalStorage< - ControllerArgs[] - >(STORAGE_KEY, []); - const navigate = useNavigate(); - - const defaultQueryParams: RegisterControllerQueryParams = { panel: null }; - const [, , handleRemovePanelQueryParams] = - usePanelQueryParams(defaultQueryParams); - - const handleRegisterAController = () => { - if (!formValues) { - return; - } - // XXX Validate form values - additionalControllers.push([ - `wss://${formValues.wsControllerHost}/api`, // wsControllerURL - { user: formValues.username || "", password: formValues.password || "" }, // credentials - formValues.identityProvider, // identityProviderAvailable - true, // additional controller - ]); - setAdditionalControllers(additionalControllers); - dispatch(appThunks.connectAndStartPolling()).catch((error) => - console.error(Label.POLLING_ERROR, error), - ); - // Close the panel - navigate(urls.controllers); - }; - - function handleInputChange(e: ChangeEvent) { - setFormValues({ - ...formValues, - [e.target.name]: - e.target.type === "checkbox" ? e.target.checked : e.target.value, - }); - } - - function generateTheControllerLink(controllerIP?: string) { - if (!controllerIP) { - return "the controller"; - } - const dashboardLink = `https://${controllerIP}/dashboard`; - return ( - - the controller - - ); - } - - return ( - - -

- The credentials are stored locally in your browser and can be - cleared on log-out. -

- - - - - - } - panelClassName="register-controller" - title={Label.TITLE} - onRemovePanelQueryParams={handleRemovePanelQueryParams} - > -

- Information can be retrieved using the juju show-controller{" "} - command. -

-
{ - event.preventDefault(); - handleRegisterAController(); - }} - > -
-
- -
- -
-
- -

- Must be a valid alpha-numeric Juju controller name.
- e.g. production-controller-aws -

-
-
-
- -
-
- -
- -
-
- -

- You'll typically want to use the public IP:Port address for the - controller.
- e.g. 91.189.88.181:17070 -

-
-
-
-
-
- { - setFormValues({ - ...formValues, - identityProvider: event.target.checked, - password: "", - username: "", - }); - }} - />{" "} - -

- This will be true for controllers with the `identity-url` - parameter set. -

-
-
-
-
- -
- -
-
- -

- The username you use to access the controller. -

-
-
-
- -
-
- -
- -
-
- -

- The password will be what you used when running{" "} - juju register or if unchanged from the default it - can be retrieved by running juju dashboard. -

-
-
-
-
-
- -
- Visit {generateTheControllerLink(formValues?.wsControllerHost)} to - accept the certificate on this controller to enable a secure - connection -
-
-
-
-
- {" "} - -
-
-
-
- ); -} diff --git a/src/panels/RegisterController/register-controller.scss b/src/panels/RegisterController/register-controller.scss deleted file mode 100644 index ce77163bd..000000000 --- a/src/panels/RegisterController/register-controller.scss +++ /dev/null @@ -1,47 +0,0 @@ -@import "vanilla-framework/scss/vanilla"; - -.register-controller { - .row { - grid-gap: 0; - padding: 0; - } - - &__warning { - position: relative; - - .p-icon--warning { - margin-top: 7px; - position: absolute; - } - } - - .p-form-help-text { - line-height: 1.25; - padding-right: 1rem; - - code { - display: inline-block; - margin: 0.25rem; - } - - &.identity-provider { - padding-left: 2rem; - } - } - - .controller-link-message { - padding-left: 2rem; - } - - .required-star { - color: $color-negative; - } - - .p-button--positive { - width: 100%; - } - - .register-a-controller__submit-segment { - padding-top: $spv--small; - } -} diff --git a/src/store/app/actions.ts b/src/store/app/actions.ts index f79184bdb..1400b0455 100644 --- a/src/store/app/actions.ts +++ b/src/store/app/actions.ts @@ -11,25 +11,14 @@ export const updatePermissions = createAction<{ wsControllerURL: string; }>("app/updatePermissions"); -export type ControllerArgs = - | [ - // wsControllerURL - string, - // credentials - Credential | undefined, - // identityProviderAvailable - boolean | undefined, - ] - | [ - // wsControllerURL - string, - // credentials - Credential | undefined, - // identityProviderAvailable - boolean | undefined, - // additional controller - boolean | undefined, - ]; +export type ControllerArgs = [ + // wsControllerURL + string, + // credentials + Credential | undefined, + // identityProviderAvailable + boolean | undefined, +]; export const connectAndPollControllers = createAction<{ controllers: ControllerArgs[]; diff --git a/src/store/app/thunks.test.ts b/src/store/app/thunks.test.ts index bb8c9c59c..7d09e7ea0 100644 --- a/src/store/app/thunks.test.ts +++ b/src/store/app/thunks.test.ts @@ -8,12 +8,10 @@ import { configFactory, } from "testing/factories/general"; import { - additionalControllerFactory, controllerFactory, jujuStateFactory, } from "testing/factories/juju/juju"; -import type { ControllerArgs } from "./actions"; import { logOut, connectAndStartPolling, connectAndListModels } from "./thunks"; describe("thunks", () => { @@ -54,41 +52,7 @@ describe("thunks", () => { expect(dispatchedThunk.type).toBe("app/connectAndListModels/fulfilled"); }); - it("connectAndStartPolling with additional controllers", async () => { - const additionalController: ControllerArgs = [ - "wss://additional.test.com", - { user: "additional", password: "additional123" }, - true, - ]; - localStorage.setItem( - "additionalControllers", - JSON.stringify([additionalController]), - ); - const action = connectAndStartPolling(); - const dispatch = jest.fn(); - const getState = jest.fn(() => rootStateFactory.build()); - await action(dispatch, getState, null); - expect(dispatch).toHaveBeenCalledWith( - generalActions.storeUserPass({ - wsControllerURL: additionalController[0], - credential: additionalController[1], - }), - ); - expect(dispatch).toHaveBeenCalledWith( - jujuActions.updateControllerList({ - wsControllerURL: additionalController[0], - controllers: [additionalControllerFactory.build()], - }), - ); - localStorage.removeItem("additionalControllers"); - }); - it("connectAndListModels", async () => { - const additionalController: ControllerArgs = [ - "wss://additional.test.com", - { user: "additional", password: "additional123" }, - true, - ]; const dispatch = jest.fn(); const getState = jest.fn(() => rootStateFactory.build({ @@ -123,14 +87,11 @@ describe("thunks", () => { }), }), ); - const action = connectAndListModels([additionalController]); + const action = connectAndListModels(); await action(dispatch, getState, null); expect(dispatch).toHaveBeenCalledWith( appActions.connectAndPollControllers({ - controllers: [ - ["wss://controller.example.com", undefined, false], - additionalController, - ], + controllers: [["wss://controller.example.com", undefined, false]], isJuju: true, }), ); diff --git a/src/store/app/thunks.ts b/src/store/app/thunks.ts index e2406478f..b8f0046a6 100644 --- a/src/store/app/thunks.ts +++ b/src/store/app/thunks.ts @@ -27,7 +27,6 @@ export const logOut = createAsyncThunk< state?.general?.config?.identityProviderAvailable; const pingerIntervalIds = getPingerIntervalIds(state); bakery.storage.clear(); - localStorage.removeItem("additionalControllers"); Object.entries(pingerIntervalIds ?? {}).forEach((pingerIntervalId) => clearInterval(pingerIntervalId[1]), ); @@ -52,40 +51,21 @@ export const connectAndStartPolling = createAsyncThunk< state: RootState; } >("app/connectAndStartPolling", async (_, thunkAPI) => { - let additionalControllers: ControllerArgs[] | null = null; try { - const data = window.localStorage.getItem("additionalControllers"); - if (data) { - additionalControllers = JSON.parse(data); - additionalControllers?.forEach((controller) => { - thunkAPI.dispatch( - generalActions.storeUserPass({ - wsControllerURL: controller[0], - credential: controller[1], - }), - ); - thunkAPI.dispatch( - jujuActions.updateControllerList({ - wsControllerURL: controller[0], - controllers: [{ additionalController: true }], - }), - ); - }); - } - await thunkAPI.dispatch(connectAndListModels(additionalControllers)); - } catch (e) { + await thunkAPI.dispatch(connectAndListModels()); + } catch (error) { // XXX Add to Sentry. - console.log("Error retrieving additional registered controllers", e); + console.error("Error while trying to connect and list models.", error); } }); export const connectAndListModels = createAsyncThunk< void, - ControllerArgs[] | null, + void, { state: RootState; } ->("app/connectAndListModels", async (additionalControllers, thunkAPI) => { +>("app/connectAndListModels", async (_, thunkAPI) => { try { const storeState = thunkAPI.getState(); const config = getConfig(storeState); @@ -100,9 +80,6 @@ export const connectAndListModels = createAsyncThunk< config?.identityProviderAvailable, ]); } - if (additionalControllers) { - controllerList = controllerList.concat(additionalControllers); - } const connectedControllers = Object.keys(controllerConnections); controllerList = controllerList.filter((controllerData) => { // remove controllers we're already connected to. diff --git a/src/store/juju/types.ts b/src/store/juju/types.ts index 16172ccac..0fd12c063 100644 --- a/src/store/juju/types.ts +++ b/src/store/juju/types.ts @@ -16,7 +16,6 @@ export type ControllerLocation = { }; export type ControllerAnnotations = { - additionalController: boolean; location?: ControllerLocation; updateAvailable?: boolean; }; @@ -31,11 +30,7 @@ export type LocalController = { export type Controller = (ControllerInfo | LocalController) & ControllerAnnotations; -export type AdditionalController = { - additionalController: true; -}; - -export type Controllers = Record; +export type Controllers = Record; // There is some model data that we don't want to store from the full status because it changes // too often causing needless re-renders and is currently irrelevant diff --git a/src/store/middleware/model-poller.test.ts b/src/store/middleware/model-poller.test.ts index 72ec5077e..8a2716305 100644 --- a/src/store/middleware/model-poller.test.ts +++ b/src/store/middleware/model-poller.test.ts @@ -202,7 +202,6 @@ describe("model poller", () => { expect(fetchControllerList).toHaveBeenCalledWith( wsControllerURL, conn, - false, fakeStore.dispatch, fakeStore.getState, ); diff --git a/src/store/middleware/model-poller.ts b/src/store/middleware/model-poller.ts index fc310d157..22ab2d6ab 100644 --- a/src/store/middleware/model-poller.ts +++ b/src/store/middleware/model-poller.ts @@ -63,12 +63,8 @@ export const modelPollerMiddleware: Middleware< // first clean up any old auth requests: reduxStore.dispatch(generalActions.clearVisitURLs()); for (const controllerData of action.payload.controllers) { - const [ - wsControllerURL, - credentials, - identityProviderAvailable, - isAdditionalController, - ] = controllerData; + const [wsControllerURL, credentials, identityProviderAvailable] = + controllerData; let conn: ConnectionWithFacades | undefined; let juju: Client | undefined; let error: unknown; @@ -175,7 +171,6 @@ export const modelPollerMiddleware: Middleware< await fetchControllerList( wsControllerURL, conn, - isAdditionalController ?? false, reduxStore.dispatch, reduxStore.getState, ); diff --git a/src/testing/factories/juju/juju.ts b/src/testing/factories/juju/juju.ts index 959523018..7e76d496c 100644 --- a/src/testing/factories/juju/juju.ts +++ b/src/testing/factories/juju/juju.ts @@ -10,7 +10,6 @@ import { Factory } from "fishery"; import { DEFAULT_AUDIT_EVENTS_LIMIT } from "store/juju/slice"; import type { - AdditionalController, AuditEventsState, Controller, ControllerLocation, @@ -31,12 +30,6 @@ function generateUUID() { }); } -export const additionalControllerFactory = Factory.define( - () => ({ - additionalController: true, - }), -); - export const controllerLocationFactory = Factory.define( () => ({ region: "aws", @@ -46,12 +39,10 @@ export const controllerLocationFactory = Factory.define( export const controllerFactory = Factory.define(() => ({ path: "admin/jaas", uuid: "a030379a-940f-4760-8fcf-3062bfake4e7", - additionalController: false, version: "1.2.3", })); export const controllerInfoFactory = Factory.define(() => ({ - additionalController: false, "agent-version": "1.2.3", name: "controller1", status: {