Skip to content

Commit

Permalink
Store the last namespace used in the browser storage (#2165)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andres Martinez Gotor committed Nov 17, 2020
1 parent 4d32f93 commit 83166b9
Show file tree
Hide file tree
Showing 11 changed files with 162 additions and 19 deletions.
9 changes: 9 additions & 0 deletions dashboard/src/actions/auth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getType } from "typesafe-actions";
import actions from ".";
import { Auth } from "../shared/Auth";
import Namespace from "../shared/Namespace";
import * as NS from "../shared/Namespace";

const defaultCluster = "default";
const mockStore = configureMockStore([thunk]);
Expand All @@ -28,6 +29,7 @@ beforeEach(() => {
Namespace.list = jest.fn(async () => {
return { namespaces: [] };
});
jest.spyOn(NS, "unsetStoredNamespace");

store = mockStore({
auth: {
Expand Down Expand Up @@ -146,3 +148,10 @@ describe("OIDC authentication", () => {
});
});
});

describe("logout", () => {
it("unsets the stored namespace", async () => {
await store.dispatch(actions.auth.logout());
expect(NS.unsetStoredNamespace).toHaveBeenCalled();
});
});
2 changes: 2 additions & 0 deletions dashboard/src/actions/auth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ThunkAction } from "redux-thunk";
import * as Namespace from "shared/Namespace";
import { ActionType, createAction } from "typesafe-actions";

import { Auth } from "../shared/Auth";
Expand Down Expand Up @@ -62,6 +63,7 @@ export function logout(): ThunkAction<
dispatch(setAuthenticated(false, false));
dispatch(clearClusters());
}
Namespace.unsetStoredNamespace();
};
}

Expand Down
20 changes: 19 additions & 1 deletion dashboard/src/actions/namespace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
receiveNamespaces,
requestNamespace,
setNamespace,
setNamespaceState,
} from "./namespace";

const mockStore = configureMockStore([thunk]);
Expand All @@ -34,7 +35,7 @@ interface ITestCase {
const actionTestCases: ITestCase[] = [
{
name: "setNamespace",
action: setNamespace,
action: setNamespaceState,
args: ["default", "jack"],
payload: { cluster: "default", namespace: "jack" },
},
Expand Down Expand Up @@ -169,3 +170,20 @@ describe("getNamespace", () => {
expect(store.getActions()).toEqual(expectedActions);
});
});

describe("setNamespace", () => {
it("dispatches namespace set", async () => {
const expectedActions = [
{
type: getType(setNamespaceState),
payload: { cluster: "default-c", namespace: "default-ns" },
},
];
await store.dispatch(setNamespace("default-c", "default-ns"));
expect(store.getActions()).toEqual(expectedActions);
expect(localStorage.setItem).toHaveBeenCalledWith(
"kubeapps_namespace",
'{"default-c":"default-ns"}',
);
});
});
16 changes: 13 additions & 3 deletions dashboard/src/actions/namespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ThunkAction } from "redux-thunk";

import { ActionType, createAction } from "typesafe-actions";

import Namespace from "../shared/Namespace";
import Namespace, { setStoredNamespace } from "../shared/Namespace";
import { IResource, IStoreState } from "../shared/types";

export const requestNamespace = createAction("REQUEST_NAMESPACE", resolve => {
Expand All @@ -12,7 +12,7 @@ export const receiveNamespace = createAction("RECEIVE_NAMESPACE", resolve => {
return (cluster: string, namespace: IResource) => resolve({ cluster, namespace });
});

export const setNamespace = createAction("SET_NAMESPACE", resolve => {
export const setNamespaceState = createAction("SET_NAMESPACE", resolve => {
return (cluster: string, namespace: string) => resolve({ cluster, namespace });
});

Expand All @@ -33,7 +33,7 @@ export const clearClusters = createAction("CLEAR_CLUSTERS");
const allActions = [
requestNamespace,
receiveNamespace,
setNamespace,
setNamespaceState,
receiveNamespaces,
errorNamespaces,
clearClusters,
Expand Down Expand Up @@ -90,3 +90,13 @@ export function getNamespace(
}
};
}

export function setNamespace(
cluster: string,
ns: string,
): ThunkAction<Promise<void>, IStoreState, null, NamespaceAction> {
return async dispatch => {
setStoredNamespace(cluster, ns);
dispatch(setNamespaceState(cluster, ns));
};
}
4 changes: 2 additions & 2 deletions dashboard/src/reducers/charts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe("chartReducer", () => {

expect(
chartsReducer(undefined, {
type: getType(actions.namespace.setNamespace) as any,
type: getType(actions.namespace.setNamespaceState) as any,
}),
).toEqual({ ...initialState });
});
Expand All @@ -51,7 +51,7 @@ describe("chartReducer", () => {
},
},
{
type: getType(actions.namespace.setNamespace) as any,
type: getType(actions.namespace.setNamespaceState) as any,
},
),
).toEqual({ ...initialState });
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/reducers/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const chartsReducer = (
isFetching: false,
selected: chartsSelectedReducer(state.selected, action),
};
case getType(actions.namespace.setNamespace):
case getType(actions.namespace.setNamespaceState):
return { ...initialState };
default:
}
Expand Down
76 changes: 73 additions & 3 deletions dashboard/src/reducers/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ describe("clusterReducer", () => {
},
},
{
type: getType(actions.namespace.setNamespace),
type: getType(actions.namespace.setNamespaceState),
payload: { cluster: "initial-cluster", namespace: "default" },
},
),
Expand Down Expand Up @@ -216,6 +216,10 @@ describe("clusterReducer", () => {
});

context("when RECEIVE_NAMESPACES", () => {
afterEach(() => {
jest.restoreAllMocks();
});

it("updates the namespace list and clears error", () => {
expect(
clusterReducer(
Expand Down Expand Up @@ -288,7 +292,6 @@ describe("clusterReducer", () => {
},
},
} as IClustersState);
jest.restoreAllMocks();
});

it("defaults to the first namespace if the one in token is not available", () => {
Expand Down Expand Up @@ -322,7 +325,6 @@ describe("clusterReducer", () => {
},
},
} as IClustersState);
jest.restoreAllMocks();
});

it("gets the existing current namespace", () => {
Expand Down Expand Up @@ -356,6 +358,74 @@ describe("clusterReducer", () => {
},
} as IClustersState);
});

it("gets the stored namespace", () => {
jest
.spyOn(window.localStorage.__proto__, "getItem")
.mockReturnValueOnce('{"other": "three"}');
expect(
clusterReducer(
{
...initialTestState,
clusters: {
other: {
currentNamespace: "",
namespaces: [],
},
},
} as IClustersState,
{
type: getType(actions.namespace.receiveNamespaces),
payload: {
cluster: "other",
namespaces: ["one", "two", "three"],
},
},
),
).toEqual({
...initialTestState,
clusters: {
other: {
currentNamespace: "three",
namespaces: ["one", "two", "three"],
error: undefined,
},
},
} as IClustersState);
});

it("ignores the stored namespace if it's not available", () => {
jest.spyOn(window.localStorage.__proto__, "getItem").mockReturnValueOnce('{"other": "four"}');
expect(
clusterReducer(
{
...initialTestState,
clusters: {
other: {
currentNamespace: "",
namespaces: [],
},
},
} as IClustersState,
{
type: getType(actions.namespace.receiveNamespaces),
payload: {
cluster: "other",
namespaces: ["one", "two", "three"],
},
},
),
).toEqual({
...initialTestState,
clusters: {
other: {
currentNamespace: "one",
namespaces: ["one", "two", "three"],
error: undefined,
},
},
} as IClustersState);
});
});

context("when RECEIVE_CONFIG", () => {
Expand Down
3 changes: 2 additions & 1 deletion dashboard/src/reducers/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,15 @@ const clusterReducer = (
...state.clusters[action.payload.cluster],
namespaces: action.payload.namespaces,
currentNamespace: getCurrentNamespace(
action.payload.cluster,
state.clusters[action.payload.cluster].currentNamespace,
action.payload.namespaces,
),
error: undefined,
},
},
};
case getType(actions.namespace.setNamespace):
case getType(actions.namespace.setNamespaceState):
return {
...state,
currentCluster: action.payload.cluster,
Expand Down
6 changes: 3 additions & 3 deletions dashboard/src/reducers/operators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe("catalogReducer", () => {
requestOperators: getType(actions.operators.requestOperators),
receiveOperators: getType(actions.operators.receiveOperators),
errorOperators: getType(actions.operators.errorOperators),
setNamespace: getType(actions.namespace.setNamespace),
setNamespaceState: getType(actions.namespace.setNamespaceState),
requestOperator: getType(actions.operators.requestOperator),
receiveOperator: getType(actions.operators.receiveOperator),
requestCSVs: getType(actions.operators.requestCSVs),
Expand Down Expand Up @@ -165,7 +165,7 @@ describe("catalogReducer", () => {

expect(
operatorReducer(undefined, {
type: actionTypes.setNamespace as any,
type: actionTypes.setNamespaceState as any,
}),
).toEqual({ ...initialState });
});
Expand All @@ -187,7 +187,7 @@ describe("catalogReducer", () => {
operators: [{} as any],
},
{
type: actionTypes.setNamespace as any,
type: actionTypes.setNamespaceState as any,
},
),
).toEqual({ ...initialState });
Expand Down
2 changes: 1 addition & 1 deletion dashboard/src/reducers/operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ const catalogReducer = (
isOLMInstalled: state.isOLMInstalled,
errors: { operator: {}, csv: {}, resource: {}, subscriptions: {} },
};
case getType(actions.namespace.setNamespace):
case getType(actions.namespace.setNamespaceState):
return { ...operatorsInitialState, isOLMInstalled: state.isOLMInstalled };
default:
return { ...state };
Expand Down
41 changes: 37 additions & 4 deletions dashboard/src/shared/Namespace.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { axiosWithAuth } from "./AxiosInstance";
import * as url from "./url";

import { get } from "lodash";
import { Auth } from "./Auth";
import { axiosWithAuth } from "./AxiosInstance";
import { ForbiddenError, IResource, NotFoundError } from "./types";
import * as url from "./url";

export default class Namespace {
public static async list(cluster: string) {
Expand Down Expand Up @@ -52,11 +52,44 @@ export const definedNamespaces = {
all: "_all",
};

export function getCurrentNamespace(currentNS: string, availableNS: string[]) {
// The namespace information will contain a map[cluster]:namespace with the default namespaces
const namespaceKey = "kubeapps_namespace";

function parseStoredNS() {
const ns = localStorage.getItem(namespaceKey) || "{}";
let parsedNS = {};
try {
parsedNS = JSON.parse(ns);
} catch (e) {
// The stored value should be a json object, if not, ignore it
}
return parsedNS;
}

function getStoredNamespace(cluster: string) {
return get(parseStoredNS(), cluster, "");
}

export function setStoredNamespace(cluster: string, namespace: string) {
const ns = parseStoredNS();
ns[cluster] = namespace;
localStorage.setItem(namespaceKey, JSON.stringify(ns));
}

export function unsetStoredNamespace() {
localStorage.removeItem(namespaceKey);
}

export function getCurrentNamespace(cluster: string, currentNS: string, availableNS: string[]) {
if (currentNS) {
// If a namespace has been already selected, use it
return currentNS;
}
// Try to get the latest namespace used
const storedNS = getStoredNamespace(cluster);
if (storedNS && availableNS.includes(storedNS)) {
return storedNS;
}
// Try to get a namespace from the auth token
const tokenNS = Auth.defaultNamespaceFromToken(Auth.getAuthToken() || "");
if (tokenNS && availableNS.includes(tokenNS)) {
Expand Down

0 comments on commit 83166b9

Please sign in to comment.