- {config?.identityProviderAvailable ? (
+ {config?.authMethod === AuthMethod.CANDID ? (
) : (
diff --git a/src/index.test.tsx b/src/index.test.tsx
index 0d234ec08..01115d5db 100644
--- a/src/index.test.tsx
+++ b/src/index.test.tsx
@@ -98,6 +98,7 @@ describe("renderApp", () => {
.mockImplementation(vi.fn());
const config = configFactory.build({
baseControllerURL: null,
+ isJuju: true,
});
window.jujuDashboardConfig = config;
renderApp();
@@ -116,6 +117,7 @@ describe("renderApp", () => {
.mockImplementation(vi.fn());
const config = configFactory.build({
baseControllerURL: null,
+ isJuju: true,
});
window.jujuDashboardConfig = config;
renderApp();
@@ -146,6 +148,7 @@ describe("renderApp", () => {
.mockImplementation(vi.fn());
const config = configFactory.build({
controllerAPIEndpoint: "/api",
+ isJuju: true,
});
window.jujuDashboardConfig = config;
renderApp();
@@ -164,6 +167,7 @@ describe("renderApp", () => {
.mockImplementation(vi.fn());
const config = configFactory.build({
controllerAPIEndpoint: "/api",
+ isJuju: true,
});
window.jujuDashboardConfig = config;
renderApp();
@@ -181,6 +185,7 @@ describe("renderApp", () => {
.mockImplementation(vi.fn());
const config = configFactory.build({
controllerAPIEndpoint: "wss://example.com/api",
+ isJuju: true,
});
window.jujuDashboardConfig = config;
renderApp();
@@ -190,7 +195,7 @@ describe("renderApp", () => {
);
});
- it("connects if there is an identity provider", async () => {
+ it("connects when using Candid", async () => {
// Mock the result of the thunk 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
@@ -203,7 +208,8 @@ describe("renderApp", () => {
.mockImplementation(vi.fn().mockResolvedValue({ catch: vi.fn() }));
const config = configFactory.build({
controllerAPIEndpoint: "wss://example.com/api",
- identityProviderAvailable: true,
+ identityProviderURL: "/candid",
+ isJuju: true,
});
window.jujuDashboardConfig = config;
renderApp();
@@ -295,7 +301,8 @@ describe("getControllerAPIEndpointErrors", () => {
);
const config = configFactory.build({
baseControllerURL: null,
- identityProviderAvailable: true,
+ identityProviderURL: "/candid",
+ isJuju: true,
});
window.jujuDashboardConfig = config;
renderApp();
diff --git a/src/index.tsx b/src/index.tsx
index 14a1e1701..7868b516f 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -10,6 +10,8 @@ import App from "components/App";
import reduxStore from "store";
import { thunks as appThunks } from "store/app";
import { actions as generalActions } from "store/general";
+import { AuthMethod } from "store/general/types";
+import type { WindowConfig } from "types";
import packageJSON from "../package.json";
@@ -29,6 +31,16 @@ if (import.meta.env.PROD && window.jujuDashboardConfig?.analyticsEnabled) {
Sentry.setTag("dashboardVersion", appVersion);
}
+const getAuthMethod = (config: WindowConfig) => {
+ if (!config.isJuju) {
+ return AuthMethod.OIDC;
+ }
+ if (config.identityProviderURL) {
+ return AuthMethod.CANDID;
+ }
+ return AuthMethod.LOCAL;
+};
+
const addressRegex = new RegExp(/^ws[s]?:\/\/(\S+)\//);
export const getControllerAPIEndpointErrors = (
controllerAPIEndpoint?: string,
@@ -80,7 +92,13 @@ export const renderApp = () => {
renderApp();
function bootstrap() {
- const config = window.jujuDashboardConfig;
+ const windowConfig = window.jujuDashboardConfig;
+ const config = windowConfig
+ ? {
+ ...windowConfig,
+ authMethod: getAuthMethod(windowConfig),
+ }
+ : null;
let error: string | null = null;
if (!config) {
error = Label.NO_CONFIG;
@@ -128,8 +146,8 @@ function bootstrap() {
reduxStore.dispatch(generalActions.storeConfig(config));
reduxStore.dispatch(generalActions.storeVersion(appVersion));
- if (config.identityProviderAvailable) {
- // If an identity provider is available then try and connect automatically
+ if (config.authMethod === AuthMethod.CANDID) {
+ // If using Candid authentication then try and connect automatically
// If not then wait for the login UI to trigger this
reduxStore
.dispatch(appThunks.connectAndStartPolling())
diff --git a/src/juju/api-hooks/common.ts b/src/juju/api-hooks/common.ts
index 7c13cb8cc..105a897a3 100644
--- a/src/juju/api-hooks/common.ts
+++ b/src/juju/api-hooks/common.ts
@@ -30,8 +30,7 @@ export const useModelConnectionCallback = (modelUUID?: string) => {
const wsControllerURL = useAppSelector((state) =>
getModelByUUID(state, modelUUID),
)?.wsControllerURL;
- const identityProviderAvailable =
- useAppSelector(getConfig)?.identityProviderAvailable;
+ const authMethod = useAppSelector(getConfig)?.authMethod;
const credentials = useAppSelector((state) =>
getUserPass(state, wsControllerURL),
);
@@ -43,12 +42,7 @@ export const useModelConnectionCallback = (modelUUID?: string) => {
// are available.
return;
}
- connectToModel(
- modelUUID,
- wsControllerURL,
- credentials,
- identityProviderAvailable,
- )
+ connectToModel(modelUUID, wsControllerURL, credentials, authMethod)
.then((connection) => {
if (!connection) {
response({ error: Label.NO_CONNECTION_ERROR });
@@ -61,7 +55,7 @@ export const useModelConnectionCallback = (modelUUID?: string) => {
response({ error: toErrorString(error) });
});
},
- [credentials, identityProviderAvailable, modelUUID, wsControllerURL],
+ [credentials, authMethod, modelUUID, wsControllerURL],
);
};
diff --git a/src/juju/api.test.ts b/src/juju/api.test.ts
index 0fbd025aa..64202578a 100644
--- a/src/juju/api.test.ts
+++ b/src/juju/api.test.ts
@@ -5,6 +5,7 @@ import { waitFor } from "@testing-library/react";
import { vi } from "vitest";
import { actions as generalActions } from "store/general";
+import { AuthMethod } from "store/general/types";
import { actions as jujuActions } from "store/juju";
import type { RootState } from "store/store";
import { rootStateFactory } from "testing/factories";
@@ -102,7 +103,7 @@ describe("Juju API", () => {
user: "eggman",
password: "123",
},
- false,
+ AuthMethod.LOCAL,
);
expect(response).toStrictEqual({
conn,
@@ -132,7 +133,7 @@ describe("Juju API", () => {
user: "eggman",
password: "123",
},
- true,
+ AuthMethod.CANDID,
);
expect(juju.login).toHaveBeenCalledWith({}, CLIENT_VERSION);
});
@@ -150,7 +151,7 @@ describe("Juju API", () => {
user: "eggman",
password: "123",
},
- false,
+ AuthMethod.LOCAL,
);
expect(response).toStrictEqual({
error: "Could not log into controller",
@@ -177,7 +178,7 @@ describe("Juju API", () => {
user: "eggman",
password: "123",
},
- false,
+ AuthMethod.LOCAL,
);
expect(ping).not.toHaveBeenCalled();
vi.advanceTimersByTime(PING_TIME);
@@ -209,7 +210,7 @@ describe("Juju API", () => {
user: "eggman",
password: "123",
},
- false,
+ AuthMethod.LOCAL,
);
vi.advanceTimersByTime(PING_TIME);
await waitFor(() => {
@@ -243,7 +244,7 @@ describe("Juju API", () => {
password: "123",
},
generateConnectionOptions(false),
- false,
+ AuthMethod.LOCAL,
);
expect(response).toStrictEqual(juju);
});
@@ -262,7 +263,7 @@ describe("Juju API", () => {
password: "123",
},
generateConnectionOptions(false),
- false,
+ AuthMethod.LOCAL,
);
vi.advanceTimersByTime(LOGIN_TIMEOUT);
await expect(response).rejects.toMatchObject(
@@ -283,7 +284,7 @@ describe("Juju API", () => {
password: "123",
},
generateConnectionOptions(false),
- false,
+ AuthMethod.LOCAL,
);
await expect(response).rejects.toMatchObject(new Error("Uh oh!"));
});
@@ -317,7 +318,7 @@ describe("Juju API", () => {
it("can log in with an external provider", async () => {
if (state.general.config) {
- state.general.config.identityProviderAvailable = true;
+ state.general.config.authMethod = AuthMethod.CANDID;
}
const loginResponse = {
conn: {
@@ -1321,7 +1322,7 @@ describe("Juju API", () => {
"abc123",
"wss://example.com/api",
credentials,
- false,
+ AuthMethod.LOCAL,
);
expect(connectAndLogin).toHaveBeenCalledWith(
"wss://example.com/model/abc123/api",
diff --git a/src/juju/api.ts b/src/juju/api.ts
index 453d5b48d..51165ad22 100644
--- a/src/juju/api.ts
+++ b/src/juju/api.ts
@@ -34,6 +34,7 @@ import {
isLoggedIn,
} from "store/general/selectors";
import type { Credential } from "store/general/types";
+import { AuthMethod } from "store/general/types";
import { actions as jujuActions } from "store/juju";
import { addControllerCloudRegion } from "store/juju/thunks";
import type {
@@ -110,10 +111,10 @@ export function generateConnectionOptions(
function determineLoginParams(
credentials: Credential | null | undefined,
- identityProviderAvailable: boolean,
+ authMethod?: AuthMethod,
) {
let loginParams: Credentials = {};
- if (credentials && !identityProviderAvailable) {
+ if (credentials && authMethod === AuthMethod.LOCAL) {
loginParams = {
username: credentials.user,
password: credentials.password,
@@ -147,7 +148,7 @@ function stopPingerLoop(intervalId: number) {
@param wsControllerURL The fully qualified URL of the controller api.
@param credentials The users credentials in the format
{user: ..., password: ...}
- @param identityProviderAvailable Whether an identity provider is available.
+ @param authMethod The method to use for authentication.
@returns
conn The controller connection instance.
juju The juju api instance.
@@ -155,16 +156,13 @@ function stopPingerLoop(intervalId: number) {
export async function loginWithBakery(
wsControllerURL: string,
credentials?: Credential,
- identityProviderAvailable: boolean = false,
+ authMethod?: AuthMethod,
) {
const juju: JujuClient = await connect(
wsControllerURL,
generateConnectionOptions(true, (e) => console.log("controller closed", e)),
);
- const loginParams = determineLoginParams(
- credentials,
- identityProviderAvailable,
- );
+ const loginParams = determineLoginParams(credentials, authMethod);
let conn: ConnectionWithFacades | null | undefined = null;
try {
conn = await juju.login(loginParams, CLIENT_VERSION);
@@ -187,22 +185,19 @@ export type LoginResponse = Awaited> & {
@param credentials The users credentials in the format
{user: ..., password: ...}
@param options The options for the connection.
- @param identityProviderAvailable If an identity provider is available.
+ @param authMethod The method to use for authentication.
@returns The full model status.
*/
export async function connectAndLoginWithTimeout(
modelURL: string,
credentials: Credential | null | undefined,
options: ConnectOptions,
- identityProviderAvailable: boolean,
+ authMethod?: AuthMethod,
): Promise {
const timeout: Promise = new Promise((_resolve, reject) => {
setTimeout(reject, LOGIN_TIMEOUT, new Error(Label.LOGIN_TIMEOUT_ERROR));
});
- const loginParams = determineLoginParams(
- credentials,
- identityProviderAvailable,
- );
+ const loginParams = determineLoginParams(credentials, authMethod);
const juju: Promise = connectAndLogin(
modelURL,
loginParams,
@@ -226,13 +221,7 @@ export async function fetchModelStatus(
getState: () => RootState,
) {
const appState = getState();
- const baseWSControllerURL = getWSControllerURL(appState);
const config = getConfig(appState);
- let useIdentityProvider = false;
-
- if (baseWSControllerURL === wsControllerURL) {
- useIdentityProvider = config?.identityProviderAvailable ?? false;
- }
const modelURL = wsControllerURL.replace("/api", `/model/${modelUUID}/api`);
let status: FullStatusWithAnnotations | null = null;
let features: ModelFeatures | null = null;
@@ -245,7 +234,7 @@ export async function fetchModelStatus(
modelURL,
controllerCredentials,
generateConnectionOptions(false),
- useIdentityProvider,
+ config?.authMethod,
);
if (isLoggedIn(getState(), wsControllerURL)) {
try {
@@ -532,14 +521,14 @@ export async function connectToModel(
modelUUID: string,
wsControllerURL: string,
credentials?: Credential,
- identityProviderAvailable = false,
+ authMethod?: AuthMethod,
) {
const modelURL = wsControllerURL.replace("/api", `/model/${modelUUID}/api`);
const response = await connectAndLoginWithTimeout(
modelURL,
credentials,
generateConnectionOptions(true),
- identityProviderAvailable,
+ authMethod,
);
return response.conn;
}
@@ -564,7 +553,7 @@ export async function connectAndLoginToModel(
modelUUID,
wsControllerURL,
credentials,
- config?.identityProviderAvailable,
+ config?.authMethod,
);
}
diff --git a/src/store/app/actions.test.ts b/src/store/app/actions.test.ts
index 6c5c23344..befe54f8d 100644
--- a/src/store/app/actions.test.ts
+++ b/src/store/app/actions.test.ts
@@ -1,3 +1,5 @@
+import { AuthMethod } from "store/general/types";
+
import type { ControllerArgs } from "./actions";
import { updatePermissions, connectAndPollControllers } from "./actions";
@@ -21,7 +23,7 @@ describe("actions", () => {
const controller: ControllerArgs = [
"wss://example.com",
{ user: "eggman@external", password: "verysecure123" },
- false,
+ AuthMethod.LOCAL,
];
const args = {
controllers: [controller],
diff --git a/src/store/app/actions.ts b/src/store/app/actions.ts
index 1400b0455..67e0440dc 100644
--- a/src/store/app/actions.ts
+++ b/src/store/app/actions.ts
@@ -1,6 +1,6 @@
import { createAction } from "@reduxjs/toolkit";
-import type { Credential } from "store/general/types";
+import type { AuthMethod, Credential } from "store/general/types";
export const updatePermissions = createAction<{
action: string;
@@ -16,8 +16,8 @@ export type ControllerArgs = [
string,
// credentials
Credential | undefined,
- // identityProviderAvailable
- boolean | undefined,
+ // authMethod
+ AuthMethod,
];
export const connectAndPollControllers = createAction<{
diff --git a/src/store/app/thunks.test.ts b/src/store/app/thunks.test.ts
index fb65ef2c7..020a10339 100644
--- a/src/store/app/thunks.test.ts
+++ b/src/store/app/thunks.test.ts
@@ -2,6 +2,7 @@ import { vi } from "vitest";
import { actions as appActions } from "store/app";
import { actions as generalActions } from "store/general";
+import { AuthMethod } from "store/general/types";
import { actions as jujuActions } from "store/juju";
import type { RootState } from "store/store";
import { rootStateFactory } from "testing/factories";
@@ -67,7 +68,7 @@ describe("thunks", () => {
rootStateFactory.build({
general: generalStateFactory.build({
config: configFactory.build({
- identityProviderAvailable: true,
+ authMethod: AuthMethod.CANDID,
}),
}),
}),
@@ -91,7 +92,9 @@ describe("thunks", () => {
await action(dispatch, getState, null);
expect(dispatch).toHaveBeenCalledWith(
appActions.connectAndPollControllers({
- controllers: [["wss://controller.example.com", undefined, false]],
+ controllers: [
+ ["wss://controller.example.com", undefined, AuthMethod.LOCAL],
+ ],
isJuju: true,
}),
);
@@ -112,7 +115,9 @@ describe("thunks", () => {
await action(dispatch, getState, null);
expect(dispatch).toHaveBeenCalledWith(
appActions.connectAndPollControllers({
- controllers: [["wss://controller.example.com", undefined, false]],
+ controllers: [
+ ["wss://controller.example.com", undefined, AuthMethod.LOCAL],
+ ],
isJuju: true,
}),
);
@@ -141,7 +146,9 @@ describe("thunks", () => {
await action(dispatch, getState, null);
expect(dispatch).toHaveBeenCalledWith(
appActions.connectAndPollControllers({
- controllers: [["wss://controller.example.com", undefined, false]],
+ controllers: [
+ ["wss://controller.example.com", undefined, AuthMethod.LOCAL],
+ ],
isJuju: true,
}),
);
@@ -171,7 +178,9 @@ describe("thunks", () => {
await action(dispatch, getState, null);
expect(dispatch).toHaveBeenCalledWith(
appActions.connectAndPollControllers({
- controllers: [["wss://controller.example.com", undefined, false]],
+ controllers: [
+ ["wss://controller.example.com", undefined, AuthMethod.LOCAL],
+ ],
isJuju: true,
}),
);
diff --git a/src/store/app/thunks.ts b/src/store/app/thunks.ts
index dce0b2664..93a788e4e 100644
--- a/src/store/app/thunks.ts
+++ b/src/store/app/thunks.ts
@@ -10,6 +10,7 @@ import {
getUserPass,
getWSControllerURL,
} from "store/general/selectors";
+import { AuthMethod } from "store/general/types";
import { actions as jujuActions } from "store/juju";
import type { RootState } from "store/store";
@@ -23,8 +24,7 @@ export const logOut = createAsyncThunk<
}
>("app/logout", async (_, thunkAPI) => {
const state = thunkAPI.getState();
- const identityProviderAvailable =
- state?.general?.config?.identityProviderAvailable;
+ const authMethod = state?.general?.config?.authMethod ?? AuthMethod.LOCAL;
const pingerIntervalIds = getPingerIntervalIds(state);
bakery.storage.clear();
Object.entries(pingerIntervalIds ?? {}).forEach((pingerIntervalId) =>
@@ -33,7 +33,7 @@ export const logOut = createAsyncThunk<
thunkAPI.dispatch(jujuActions.clearModelData());
thunkAPI.dispatch(jujuActions.clearControllerData());
thunkAPI.dispatch(generalActions.logOut());
- if (identityProviderAvailable) {
+ if (authMethod === AuthMethod.CANDID) {
// To enable users to log back in after logging out we have to re-connect
// to the controller to get another wait url and start polling on it
// again.
@@ -62,7 +62,7 @@ export const connectAndStartPolling = createAsyncThunk<
controllerList.push([
wsControllerURL,
credentials,
- config?.identityProviderAvailable,
+ config?.authMethod ?? AuthMethod.LOCAL,
]);
}
const connectedControllers = Object.keys(controllerConnections);
diff --git a/src/store/general/selectors.ts b/src/store/general/selectors.ts
index 0ef7442d1..b4487483f 100644
--- a/src/store/general/selectors.ts
+++ b/src/store/general/selectors.ts
@@ -13,7 +13,7 @@ const slice = (state: RootState) => state.general;
*/
export const getConfig = createSelector(
[slice],
- (sliceState) => sliceState?.config,
+ (sliceState) => sliceState.config,
);
/**
diff --git a/src/store/general/types.ts b/src/store/general/types.ts
index 1af90c2b3..b36e0ea7f 100644
--- a/src/store/general/types.ts
+++ b/src/store/general/types.ts
@@ -1,14 +1,20 @@
import type { ConnectionInfo } from "@canonical/jujulib";
+export enum AuthMethod {
+ CANDID = "candid",
+ LOCAL = "local",
+ OIDC = "oidc",
+}
+
export type Config = {
- controllerAPIEndpoint: string;
+ analyticsEnabled: boolean;
+ authMethod: AuthMethod;
baseAppURL: string;
// Support for 2.9 configuration.
baseControllerURL?: string | null;
- identityProviderAvailable: boolean;
+ controllerAPIEndpoint: string;
identityProviderURL: string;
isJuju: boolean;
- analyticsEnabled: boolean;
};
export type ControllerConnections = Record;
diff --git a/src/store/middleware/model-poller.test.ts b/src/store/middleware/model-poller.test.ts
index 50c870ce8..dc53df830 100644
--- a/src/store/middleware/model-poller.test.ts
+++ b/src/store/middleware/model-poller.test.ts
@@ -8,6 +8,7 @@ import type { RelationshipTuple } from "juju/jimm/JIMMV4";
import { actions as appActions, thunks as appThunks } from "store/app";
import type { ControllerArgs } from "store/app/actions";
import { actions as generalActions } from "store/general";
+import { AuthMethod } from "store/general/types";
import { actions as jujuActions } from "store/juju";
import { rootStateFactory } from "testing/factories";
import { generalStateFactory } from "testing/factories/general";
@@ -44,7 +45,11 @@ describe("model poller", () => {
const originalLog = console.log;
const wsControllerURL = "wss://example.com";
const controllers: ControllerArgs[] = [
- [wsControllerURL, { user: "eggman@external", password: "test" }, false],
+ [
+ wsControllerURL,
+ { user: "eggman@external", password: "test" },
+ AuthMethod.LOCAL,
+ ],
];
const models = [
{
@@ -182,7 +187,7 @@ describe("model poller", () => {
password: "test",
user: "eggman@external",
},
- false,
+ AuthMethod.LOCAL,
],
);
});
@@ -384,7 +389,11 @@ describe("model poller", () => {
it("disables masking when using JIMM", async () => {
const controllers = [
- [wsControllerURL, { user: "eggman@external", password: "test" }, true],
+ [
+ wsControllerURL,
+ { user: "eggman@external", password: "test" },
+ AuthMethod.CANDID,
+ ],
];
const disableControllerUUIDMasking = vi.spyOn(
jujuModule,
diff --git a/src/store/middleware/model-poller.ts b/src/store/middleware/model-poller.ts
index a03bffa4e..6d9dd66a8 100644
--- a/src/store/middleware/model-poller.ts
+++ b/src/store/middleware/model-poller.ts
@@ -16,6 +16,7 @@ import type { ConnectionWithFacades } from "juju/types";
import { actions as appActions, thunks as appThunks } from "store/app";
import { actions as generalActions } from "store/general";
import { isLoggedIn } from "store/general/selectors";
+import { AuthMethod } from "store/general/types";
import { actions as jujuActions } from "store/juju";
import type { RootState, Store } from "store/store";
import { isSpecificAction } from "types";
@@ -71,8 +72,7 @@ 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] =
- controllerData;
+ const [wsControllerURL, credentials, authMethod] = controllerData;
let conn: ConnectionWithFacades | undefined;
let juju: Client | undefined;
let error: unknown;
@@ -81,7 +81,7 @@ export const modelPollerMiddleware: Middleware<
({ conn, error, juju, intervalId } = await loginWithBakery(
wsControllerURL,
credentials,
- identityProviderAvailable,
+ authMethod,
));
if (conn) {
controllers.set(wsControllerURL, conn);
@@ -190,7 +190,7 @@ export const modelPollerMiddleware: Middleware<
reduxStore.dispatch,
reduxStore.getState,
);
- if (identityProviderAvailable) {
+ if (authMethod === AuthMethod.CANDID) {
// This call will be a noop if the user isn't an administrator
// on the JIMM controller we're connected to.
try {
diff --git a/src/testing/factories/general.ts b/src/testing/factories/general.ts
index 1ee3959b6..309cddfaa 100644
--- a/src/testing/factories/general.ts
+++ b/src/testing/factories/general.ts
@@ -1,17 +1,18 @@
import { Factory } from "fishery";
-import type {
- Config,
- ControllerFeatures,
- Credential,
- GeneralState,
- ControllerFeaturesState,
+import {
+ type Config,
+ type ControllerFeatures,
+ type Credential,
+ type GeneralState,
+ type ControllerFeaturesState,
+ AuthMethod,
} from "store/general/types";
export const configFactory = Factory.define(() => ({
+ authMethod: AuthMethod.LOCAL,
controllerAPIEndpoint: "wss://controller.example.com",
baseAppURL: "/",
- identityProviderAvailable: false,
identityProviderURL: "",
isJuju: false,
analyticsEnabled: true,
diff --git a/src/types.ts b/src/types.ts
index 2db4886c7..ece9e126a 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -4,9 +4,11 @@ import { isAction } from "redux";
import type { Config } from "store/general/types";
+export type WindowConfig = Omit;
+
declare global {
interface Window {
- jujuDashboardConfig?: Config;
+ jujuDashboardConfig?: WindowConfig;
}
}