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

WD-8412 - Hide secrets tab #1696

Merged
merged 1 commit into from
Feb 4, 2024
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
107 changes: 102 additions & 5 deletions src/juju/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,12 +320,12 @@ describe("Juju API", () => {
});

it("handles a logged out user", async () => {
const response = await fetchModelStatus(
const { status } = await fetchModelStatus(
"abc123",
"wss://example.com/api",
() => rootStateFactory.build(),
);
expect(response).toBeNull();
expect(status).toBeNull();
});

it("can log in with an external provider", async () => {
Expand Down Expand Up @@ -394,7 +394,7 @@ describe("Juju API", () => {
jest
.spyOn(jujuLib, "connectAndLogin")
.mockImplementation(async () => loginResponse);
const response = await fetchModelStatus(
const { status: response } = await fetchModelStatus(
"abc123",
"wss://example.com/api",
() => state,
Expand Down Expand Up @@ -431,7 +431,7 @@ describe("Juju API", () => {
jest
.spyOn(jujuLib, "connectAndLogin")
.mockImplementation(async () => loginResponse);
const response = await fetchModelStatus(
const { status: response } = await fetchModelStatus(
"abc123",
"wss://example.com/api",
() => state,
Expand All @@ -441,6 +441,90 @@ describe("Juju API", () => {
});
});

it("handles features when no secrets facade", async () => {
const status = fullStatusFactory.build();
const loginResponse = {
conn: {
facades: {
client: {
fullStatus: jest.fn().mockReturnValue(status),
},
},
} as unknown as Connection,
logout: jest.fn(),
};
jest
.spyOn(jujuLib, "connectAndLogin")
.mockImplementation(async () => loginResponse);
const { features } = await fetchModelStatus(
"abc123",
"wss://example.com/api",
() => state,
);
expect(features).toStrictEqual({
listSecrets: false,
manageSecrets: false,
});
});

it("handles features when secrets facade is v1", async () => {
const status = fullStatusFactory.build();
const loginResponse = {
conn: {
facades: {
client: {
fullStatus: jest.fn().mockReturnValue(status),
},
secrets: {
VERSION: 1,
},
},
} as unknown as Connection,
logout: jest.fn(),
};
jest
.spyOn(jujuLib, "connectAndLogin")
.mockImplementation(async () => loginResponse);
const { features } = await fetchModelStatus(
"abc123",
"wss://example.com/api",
() => state,
);
expect(features).toStrictEqual({
listSecrets: true,
manageSecrets: false,
});
});

it("handles features when secrets facade is v2", async () => {
const status = fullStatusFactory.build();
const loginResponse = {
conn: {
facades: {
client: {
fullStatus: jest.fn().mockReturnValue(status),
},
secrets: {
VERSION: 2,
},
},
} as unknown as Connection,
logout: jest.fn(),
};
jest
.spyOn(jujuLib, "connectAndLogin")
.mockImplementation(async () => loginResponse);
const { features } = await fetchModelStatus(
"abc123",
"wss://example.com/api",
() => state,
);
expect(features).toStrictEqual({
listSecrets: true,
manageSecrets: true,
});
});

it("handles string error responses", async () => {
const consoleError = console.error;
console.error = jest.fn();
Expand Down Expand Up @@ -491,14 +575,17 @@ describe("Juju API", () => {
});
});

it("can fetch and store model status", async () => {
it("can fetch and store model status and features", async () => {
const status = fullStatusFactory.build();
const loginResponse = {
conn: {
facades: {
client: {
fullStatus: jest.fn().mockReturnValue(status),
},
secrets: {
VERSION: 1,
},
},
} as unknown as Connection,
logout: jest.fn(),
Expand All @@ -520,6 +607,16 @@ describe("Juju API", () => {
wsControllerURL: "wss://example.com/api",
}),
);
expect(dispatch).toHaveBeenCalledWith(
jujuActions.updateModelFeatures({
modelUUID: "abc123",
features: {
listSecrets: true,
manageSecrets: false,
},
wsControllerURL: "wss://example.com/api",
}),
);
});

it("handles no model status returned", async () => {
Expand Down
33 changes: 25 additions & 8 deletions src/juju/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ import {
import type { Credential } from "store/general/types";
import { actions as jujuActions } from "store/juju";
import { addControllerCloudRegion } from "store/juju/thunks";
import type { Controller as JujuController } from "store/juju/types";
import type {
Controller as JujuController,
ModelFeatures,
} from "store/juju/types";
import { ModelsError } from "store/middleware/model-poller";
import type { RootState, Store } from "store/store";

Expand Down Expand Up @@ -244,6 +247,7 @@ export async function fetchModelStatus(
}
const modelURL = wsControllerURL.replace("/api", `/model/${modelUUID}/api`);
let status: FullStatusWithAnnotations | string | null = null;
let features: ModelFeatures | null = null;
// Logged in state is checked multiple times as the user may have logged out
// between requests.
if (isLoggedIn(getState(), wsControllerURL)) {
Expand Down Expand Up @@ -284,14 +288,19 @@ export async function fetchModelStatus(
}
});
status.annotations = annotations;
const secretsFacade = conn?.facades.secrets?.VERSION ?? 0;
features = {
listSecrets: secretsFacade >= 1,
manageSecrets: secretsFacade >= 2,
};
}
logout();
} catch (error) {
console.error("Error connecting to model:", modelUUID, error);
throw error;
}
}
return status;
return { status, features };
}

/**
Expand All @@ -307,13 +316,21 @@ export async function fetchAndStoreModelStatus(
dispatch: Dispatch,
getState: () => RootState,
) {
const status = await fetchModelStatus(modelUUID, wsControllerURL, getState);
if (!status) {
return;
}
dispatch(
jujuActions.updateModelStatus({ modelUUID, status, wsControllerURL }),
const { status, features } = await fetchModelStatus(
modelUUID,
wsControllerURL,
getState,
);
if (status) {
dispatch(
jujuActions.updateModelStatus({ modelUUID, status, wsControllerURL }),
);
}
if (features) {
dispatch(
jujuActions.updateModelFeatures({ modelUUID, features, wsControllerURL }),
);
}
}

/**
Expand Down
27 changes: 24 additions & 3 deletions src/pages/EntityDetails/EntityDetails.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ import {
configFactory,
} from "testing/factories/general";
import { modelListInfoFactory } from "testing/factories/juju/juju";
import { controllerFactory } from "testing/factories/juju/juju";
import {
controllerFactory,
modelFeaturesStateFactory,
modelFeaturesFactory,
} from "testing/factories/juju/juju";
import {
applicationInfoFactory,
modelWatcherModelDataFactory,
Expand Down Expand Up @@ -116,7 +120,7 @@ describe("Entity Details Container", () => {
it("lists the correct tabs", () => {
renderComponent(<EntityDetails />, { path, url, state });
expect(screen.getByTestId("view-selector")).toHaveTextContent(
/^ApplicationsIntegrationsLogsSecretsMachines$/,
/^ApplicationsIntegrationsLogsMachines$/,
);
});

Expand All @@ -135,11 +139,28 @@ describe("Entity Details Container", () => {
};
renderComponent(<EntityDetails />, { path, url, state });
expect(screen.getByTestId("view-selector")).toHaveTextContent(
/^ApplicationsIntegrationsLogsSecrets$/,
/^ApplicationsIntegrationsLogs$/,
);
});

it("lists the secrets tab", () => {
state.juju.modelFeatures = modelFeaturesStateFactory.build({
abc123: modelFeaturesFactory.build({
listSecrets: true,
}),
});
renderComponent(<EntityDetails />, { path, url, state });
expect(screen.getByTestId("view-selector")).toHaveTextContent(
/^ApplicationsIntegrationsLogsSecretsMachines$/,
);
});

it("clicking the tabs changes the visible section", async () => {
state.juju.modelFeatures = modelFeaturesStateFactory.build({
abc123: modelFeaturesFactory.build({
listSecrets: true,
}),
});
renderComponent(<EntityDetails />, { path, url, state });
const viewSelector = screen.getByTestId("view-selector");
const sections = [
Expand Down
14 changes: 10 additions & 4 deletions src/pages/EntityDetails/EntityDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import useWindowTitle from "hooks/useWindowTitle";
import BaseLayout from "layout/BaseLayout/BaseLayout";
import { getIsJuju, getUserPass } from "store/general/selectors";
import {
getCanListSecrets,
getControllerDataByUUID,
getModelInfo,
getModelListLoaded,
Expand Down Expand Up @@ -81,7 +82,9 @@ const EntityDetails = ({ modelWatcherError }: Props) => {
getControllerDataByUUID(controllerUUID),
);
const entityType = getEntityType(routeParams);

const canListSecrets = useAppSelector((state) =>
getCanListSecrets(state, modelUUID),
);
const credentials = useAppSelector((state) =>
getUserPass(state, primaryControllerData?.[0]),
);
Expand Down Expand Up @@ -138,14 +141,17 @@ const EntityDetails = ({ modelWatcherError }: Props) => {
to: urls.model.tab({ userName, modelName, tab: ModelTab.LOGS }),
component: Link,
},
{
];

if (canListSecrets) {
items.push({
active: activeView === "secrets",
label: "Secrets",
onClick: (e: MouseEvent) => handleNavClick(e),
to: urls.model.tab({ userName, modelName, tab: ModelTab.SECRETS }),
component: Link,
},
];
});
}

if (modelInfo?.type !== "kubernetes") {
items.push({
Expand Down
25 changes: 25 additions & 0 deletions src/pages/EntityDetails/Model/Model.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
modelDataFactory,
modelDataInfoFactory,
modelListInfoFactory,
modelFeaturesStateFactory,
modelFeaturesFactory,
} from "testing/factories/juju/juju";
import {
applicationInfoFactory,
Expand Down Expand Up @@ -435,6 +437,11 @@ describe("Model", () => {
});

it("can display the secrets tab via the URL", async () => {
state.juju.modelFeatures = modelFeaturesStateFactory.build({
abc123: modelFeaturesFactory.build({
listSecrets: true,
}),
});
renderComponent(<Model />, {
state,
url: "/models/eggman@external/test1?activeView=secrets",
Expand All @@ -447,6 +454,24 @@ describe("Model", () => {
).toBeInTheDocument();
});

it("does not display the secrets tab if the feature is not available", async () => {
state.juju.modelFeatures = modelFeaturesStateFactory.build({
abc123: modelFeaturesFactory.build({
listSecrets: false,
}),
});
renderComponent(<Model />, {
state,
url: "/models/eggman@external/test1?activeView=secrets",
path,
});
expect(
within(screen.getByTestId(TestId.MAIN)).queryByTestId(
SecretsTestId.SECRETS_TAB,
),
).not.toBeInTheDocument();
});

it("renders the details pane for models shared-with-me", () => {
state.juju.modelWatcherData = {
abc123: modelWatcherModelDataFactory.build({
Expand Down
9 changes: 7 additions & 2 deletions src/pages/EntityDetails/Model/Model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
getModelUnits,
getModelUUIDFromList,
} from "store/juju/selectors";
import { getModelCredential } from "store/juju/selectors";
import { getModelCredential, getCanListSecrets } from "store/juju/selectors";
import { extractCloudName } from "store/juju/utils/models";
import { useAppSelector } from "store/store";
import {
Expand Down Expand Up @@ -93,6 +93,9 @@ const Model = () => {
const relations = useSelector(getModelRelations(modelUUID));
const machines = useSelector(getModelMachines(modelUUID));
const units = useSelector(getModelUnits(modelUUID));
const canListSecrets = useAppSelector((state) =>
getCanListSecrets(state, modelUUID),
);
const canConfigureModel = useCanConfigureModel();

const machinesTableRows = useMemo(() => {
Expand Down Expand Up @@ -245,7 +248,9 @@ const Model = () => {
</>
)}
{shouldShow("logs", query.activeView) && <Logs />}
{shouldShow("secrets", query.activeView) && <Secrets />}
{shouldShow("secrets", query.activeView) && canListSecrets ? (
<Secrets />
) : null}
</div>
</>
);
Expand Down
Loading
Loading