diff --git a/package.json b/package.json index 55b604955..686dfd90b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "juju-dashboard", - "version": "0.10.0", + "version": "0.11.0", "description": "A dashboard for Juju and JAAS (Juju as a service)", "bugs": { "url": "https://github.com/canonical-web-and-design/jaas-dashboard/issues" @@ -48,7 +48,7 @@ ] }, "dependencies": { - "@canonical/jujulib": "3.1.1", + "@canonical/jujulib": "3.2.0", "@canonical/macaroon-bakery": "1.3.2", "@canonical/react-components": "0.38.0", "@reduxjs/toolkit": "1.9.3", diff --git a/src/juju/api.test.ts b/src/juju/api.test.ts index bf77bfeb5..1981c955c 100644 --- a/src/juju/api.test.ts +++ b/src/juju/api.test.ts @@ -16,6 +16,7 @@ import { fetchModelStatus, generateConnectionOptions, loginWithBakery, + CLIENT_VERSION, } from "./api"; jest.mock("@canonical/jujulib", () => ({ @@ -70,10 +71,13 @@ describe("Juju API", () => { intervalId: expect.any(Number), }); expect(connectSpy).toHaveBeenCalled(); - expect(juju.login).toHaveBeenCalledWith({ - username: "eggman", - password: "123", - }); + expect(juju.login).toHaveBeenCalledWith( + { + username: "eggman", + password: "123", + }, + CLIENT_VERSION + ); }); it("handles login with external provider", async () => { @@ -90,7 +94,7 @@ describe("Juju API", () => { }, true ); - expect(juju.login).toHaveBeenCalledWith({}); + expect(juju.login).toHaveBeenCalledWith({}, CLIENT_VERSION); }); it("handles login errors", async () => { @@ -264,7 +268,8 @@ describe("Juju API", () => { expect.any(String), // An empty object is passed when using an external provider. {}, - expect.any(Object) + expect.any(Object), + CLIENT_VERSION ); }); diff --git a/src/juju/api.ts b/src/juju/api.ts index 183b3302b..94e60523c 100644 --- a/src/juju/api.ts +++ b/src/juju/api.ts @@ -15,7 +15,7 @@ import Annotations from "@canonical/jujulib/dist/api/facades/annotations"; import Application from "@canonical/jujulib/dist/api/facades/application"; import type { ErrorResults } from "@canonical/jujulib/dist/api/facades/application/ApplicationV15"; import Charms from "@canonical/jujulib/dist/api/facades/charms"; -import type { Charm } from "@canonical/jujulib/dist/api/facades/charms/CharmsV2"; +import type { Charm } from "@canonical/jujulib/dist/api/facades/charms/CharmsV5"; import Client from "@canonical/jujulib/dist/api/facades/client"; import Cloud from "@canonical/jujulib/dist/api/facades/cloud"; import Controller from "@canonical/jujulib/dist/api/facades/controller"; @@ -57,6 +57,12 @@ import type { export const PING_TIME = 20000; export const LOGIN_TIMEOUT = 5000; +// Juju supports a client one major version away from the controller's version, +// but only when the minor version is `0` so by setting this to exactly `3.0.0` +// this will allow the dashboard to work with both 2.x.x and 3.x.x controllers. +// See the API server code for more details: +// https://github.com/juju/juju/blob/e2c7b4c88e516976666e3d0c9479d0d3c704e643/apiserver/restrict_newer_client.go#L21C1-L29 +export const CLIENT_VERSION = "3.0.0"; /** Return a common connection option config. @@ -151,7 +157,7 @@ export async function loginWithBakery( ); let conn: ConnectionWithFacades | null | undefined = null; try { - conn = await juju.login(loginParams); + conn = await juju.login(loginParams, CLIENT_VERSION); } catch (error) { return { error }; } @@ -191,7 +197,8 @@ export async function connectAndLoginWithTimeout( const juju: Promise = connectAndLogin( modelURL, loginParams, - options + options, + CLIENT_VERSION ); return new Promise((resolve, reject) => { Promise.race([timeout, juju]).then((resp) => { @@ -404,7 +411,7 @@ export async function fetchControllerList( const controllerConfig = await conn.facades.controller?.controllerConfig( null ); - if (controllerConfig) { + if (controllerConfig?.config) { controllers = [ { path: controllerConfig.config["controller-name"], diff --git a/src/panels/ConfigPanel/ConfigPanel.test.tsx b/src/panels/ConfigPanel/ConfigPanel.test.tsx index 3c7a86697..d85d583e3 100644 --- a/src/panels/ConfigPanel/ConfigPanel.test.tsx +++ b/src/panels/ConfigPanel/ConfigPanel.test.tsx @@ -9,7 +9,7 @@ import type { RootState } from "store/store"; import { applicationGetFactory, configFactory, -} from "testing/factories/juju/ApplicationV15"; +} from "testing/factories/juju/Application"; import { modelUserInfoFactory } from "testing/factories/juju/ModelManagerV9"; import { controllerFactory, diff --git a/src/panels/ShareModelPanel/ShareModel.test.tsx b/src/panels/ShareModelPanel/ShareModel.test.tsx index c6bd6bcec..d4c4c24aa 100644 --- a/src/panels/ShareModelPanel/ShareModel.test.tsx +++ b/src/panels/ShareModelPanel/ShareModel.test.tsx @@ -44,6 +44,7 @@ describe("Share Model Panel", () => { modelUserInfoFactory.build({ user: "eggman@external" }), modelUserInfoFactory.build({ user: "spaceman@domain" }), ], + uuid: "abc123", }), }), }, @@ -74,6 +75,8 @@ describe("Share Model Panel", () => { state.juju.modelData.def456 = modelDataFactory.build({ info: modelDataInfoFactory.build({ users: [ + modelUserInfoFactory.build({ user: "eggman@external" }), + modelUserInfoFactory.build({ user: "another@external" }), modelUserInfoFactory.build({ user: "other@model2" }), modelUserInfoFactory.build({ user: "other2@anothermodel2" }), ], @@ -197,7 +200,7 @@ describe("Share Model Panel", () => { ); expect(updatePermissionsSpy).toHaveBeenCalledWith({ action: "revoke", - modelUUID: "84e872ff-9171-46be-829b-70f0ffake18d", + modelUUID: "abc123", permissionFrom: "read", permissionTo: undefined, user: "spaceman@domain", diff --git a/src/panels/ShareModelPanel/ShareModel.tsx b/src/panels/ShareModelPanel/ShareModel.tsx index b983eecfd..ee1be06ca 100644 --- a/src/panels/ShareModelPanel/ShareModel.tsx +++ b/src/panels/ShareModelPanel/ShareModel.tsx @@ -73,8 +73,11 @@ export default function ShareModel() { const modelUserDomains = useAppSelector((state) => getUserDomainsInModel(state, modelUUID) ); + const allDomains = allUserDomains.filter( + (domain) => !modelUserDomains.includes(domain) + ); // Display the domains used in this model first. - const userDomains = [...modelUserDomains, ...allUserDomains].slice(0, 5); + const userDomains = [...modelUserDomains, ...allDomains].slice(0, 5); const modelControllerURL = modelControllerData?.url; const users = modelStatusData?.info?.users; diff --git a/src/store/juju/slice.ts b/src/store/juju/slice.ts index 227a399da..cfe3e9ae7 100644 --- a/src/store/juju/slice.ts +++ b/src/store/juju/slice.ts @@ -124,6 +124,9 @@ const slice = createSlice({ if (!state.modelWatcherData) { state.modelWatcherData = {}; } + if (!state.modelWatcherData[action.payload.uuid]) { + return; + } state.modelWatcherData[action.payload.uuid].model = { ...(state.modelWatcherData[action.payload.uuid]?.model ?? {}), "cloud-tag": action.payload.status.model["cloud-tag"], diff --git a/src/store/juju/types.ts b/src/store/juju/types.ts index c86c6a90d..6f0515ee5 100644 --- a/src/store/juju/types.ts +++ b/src/store/juju/types.ts @@ -1,4 +1,4 @@ -import type { Charm } from "@canonical/jujulib/dist/api/facades/charms/CharmsV4"; +import type { Charm } from "@canonical/jujulib/dist/api/facades/charms/CharmsV5"; import type { FullStatus } from "@canonical/jujulib/dist/api/facades/client/ClientV6"; import type { ModelInfo as JujuModelInfo } from "@canonical/jujulib/dist/api/facades/model-manager/ModelManagerV9"; diff --git a/src/testing/factories/juju/ApplicationV15.ts b/src/testing/factories/juju/Application.ts similarity index 98% rename from src/testing/factories/juju/ApplicationV15.ts rename to src/testing/factories/juju/Application.ts index a3962b8ca..7836ea391 100644 --- a/src/testing/factories/juju/ApplicationV15.ts +++ b/src/testing/factories/juju/Application.ts @@ -33,6 +33,7 @@ export const constraintsFactory = Factory.define(() => ({ container: "", cores: 0, "cpu-power": 0, + "image-id": "123", "instance-role": "", "instance-type": "", mem: 0, diff --git a/src/testing/factories/juju/Charms.ts b/src/testing/factories/juju/Charms.ts index aafc17c31..ee500fed6 100644 --- a/src/testing/factories/juju/Charms.ts +++ b/src/testing/factories/juju/Charms.ts @@ -1,4 +1,4 @@ -import type { Charm } from "@canonical/jujulib/dist/api/facades/charms/CharmsV2"; +import type { Charm } from "@canonical/jujulib/dist/api/facades/charms/CharmsV5"; import { Factory } from "fishery"; import type { ApplicationInfo } from "juju/types"; diff --git a/yarn.lock b/yarn.lock index c64d2f71d..73081ee6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1089,10 +1089,10 @@ resolved "https://registry.yarnpkg.com/@canonical/cookie-policy/-/cookie-policy-3.4.0.tgz#0d6708da340df5867fd2cc9dbd95538c46f20cf8" integrity sha512-cdVqxQmGu+j+Q86UobihWWVFzGzHlekFeMFxlbRpm+yqxEOUCrLkA9/t/RsMfLNDToP2ECPgsMbS20aPlA2tIg== -"@canonical/jujulib@3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@canonical/jujulib/-/jujulib-3.1.1.tgz#6b8776a55adb7ab80f4a66775facbebdc0c9e4ab" - integrity sha512-8Gm0rMnWtN805KR5aOQUST+q6xr6R9Zkrnv/EqlDoccAJ+k/28ccjHtvWQKVn8OVQnpLPIM37r7m8z5SuBqNxg== +"@canonical/jujulib@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@canonical/jujulib/-/jujulib-3.2.0.tgz#7e0d29060bb100a265fc54b077200a7f894e2c1a" + integrity sha512-YRrio9rxoaUQ9c3/RlQw3Sde8FtmqpClYsGtnhtftAnJ6BV1WKfbz6VLSCU5EO5srDZNVx2vxgBUzgk382kL1w== dependencies: "@canonical/macaroon-bakery" "1.3.2" btoa "1.2.1"