diff --git a/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.html b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.html
index ee8474a2a..df999b9db 100644
--- a/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.html
+++ b/app/kubernetes/registries/kube-registry-access-view/kube-registry-access-view.html
@@ -1,4 +1,4 @@
-
+
diff --git a/app/kubernetes/services/namespaceService.js b/app/kubernetes/services/namespaceService.js
index a5ff56ca4..c888b5b88 100644
--- a/app/kubernetes/services/namespaceService.js
+++ b/app/kubernetes/services/namespaceService.js
@@ -4,11 +4,13 @@ import { KubernetesCommonParams } from 'Kubernetes/models/common/params';
import KubernetesNamespaceConverter from 'Kubernetes/converters/namespace';
import { updateNamespaces } from 'Kubernetes/store/namespace';
import $allSettled from 'Portainer/services/allSettled';
+import { getSelfSubjectAccessReview } from '@/react/kubernetes/namespaces/getSelfSubjectAccessReview';
class KubernetesNamespaceService {
/* @ngInject */
- constructor($async, KubernetesNamespaces, LocalStorage) {
+ constructor($async, KubernetesNamespaces, LocalStorage, $state) {
this.$async = $async;
+ this.$state = $state;
this.KubernetesNamespaces = KubernetesNamespaces;
this.LocalStorage = LocalStorage;
@@ -66,8 +68,10 @@ class KubernetesNamespaceService {
try {
// get the list of all namespaces (RBAC allows users to see the list of namespaces)
const data = await this.KubernetesNamespaces().get().$promise;
- // get the status of each namespace (RBAC will give permission denied for status of unauthorised namespaces)
- const promises = data.items.map((item) => this.KubernetesNamespaces().status({ id: item.metadata.name }).$promise);
+ // get the status of each namespace with accessReviews (to avoid failed forbidden responses, which aren't cached)
+ const accessReviews = await Promise.all(data.items.map((namespace) => getSelfSubjectAccessReview(this.$state.params.endpointId, namespace.metadata.name)));
+ const allowedNamespaceNames = accessReviews.filter((ar) => ar.status.allowed).map((ar) => ar.spec.resourceAttributes.namespace);
+ const promises = allowedNamespaceNames.map((name) => this.KubernetesNamespaces().status({ id: name }).$promise);
const namespaces = await $allSettled(promises);
// only return namespaces if the user has access to namespaces
const allNamespaces = namespaces.fulfilled.map((item) => {
diff --git a/app/kubernetes/views/resource-pools/resourcePoolsController.js b/app/kubernetes/views/resource-pools/resourcePoolsController.js
index c9353812c..7899375fd 100644
--- a/app/kubernetes/views/resource-pools/resourcePoolsController.js
+++ b/app/kubernetes/views/resource-pools/resourcePoolsController.js
@@ -75,7 +75,7 @@ class KubernetesResourcePoolsController {
async getResourcePoolsAsync() {
try {
- this.resourcePools = await this.KubernetesResourcePoolService.get('', { getQuota: true });
+ this.resourcePools = await this.KubernetesResourcePoolService.get();
} catch (err) {
this.Notifications.error('Failure', err, 'Unable to retreive namespaces');
}
diff --git a/app/portainer/components/theme/theme-settings.controller.js b/app/portainer/components/theme/theme-settings.controller.js
index c0d5b9922..3b847ced6 100644
--- a/app/portainer/components/theme/theme-settings.controller.js
+++ b/app/portainer/components/theme/theme-settings.controller.js
@@ -1,5 +1,5 @@
import { notifyError, notifySuccess } from '@/portainer/services/notifications';
-import { queryKeys } from '@/portainer/users/queries/queryKeys';
+import { userQueryKeys } from '@/portainer/users/queries/queryKeys';
import { queryClient } from '@/react-tools/react-query';
import { options } from '@/react/portainer/account/AccountView/theme-options';
@@ -32,7 +32,7 @@ export default class ThemeSettingsController {
try {
if (!this.state.isDemo) {
await this.UserService.updateUserTheme(this.state.userId, theme);
- await queryClient.invalidateQueries(queryKeys.user(this.state.userId));
+ await queryClient.invalidateQueries(userQueryKeys.user(this.state.userId));
}
notifySuccess('Success', 'User theme settings successfully updated');
diff --git a/app/portainer/models/user.js b/app/portainer/models/user.js
index 676d4f37a..a8979a059 100644
--- a/app/portainer/models/user.js
+++ b/app/portainer/models/user.js
@@ -12,6 +12,7 @@ export function UserViewModel(data) {
}
this.AuthenticationMethod = data.AuthenticationMethod;
this.Checked = false;
+ this.UseCache = data.UseCache;
}
export function UserTokenModel(data) {
diff --git a/app/portainer/react/components/account.ts b/app/portainer/react/components/account.ts
new file mode 100644
index 000000000..cb75d29c4
--- /dev/null
+++ b/app/portainer/react/components/account.ts
@@ -0,0 +1,17 @@
+import angular from 'angular';
+
+import { r2a } from '@/react-tools/react2angular';
+import { withCurrentUser } from '@/react-tools/withCurrentUser';
+import { withUIRouter } from '@/react-tools/withUIRouter';
+import { withReactQuery } from '@/react-tools/withReactQuery';
+import { ApplicationSettingsWidget } from '@/react/portainer/account/AccountView/ApplicationSettings';
+
+export const accountModule = angular
+ .module('portainer.app.react.components.account', [])
+ .component(
+ 'applicationSettingsWidget',
+ r2a(
+ withUIRouter(withReactQuery(withCurrentUser(ApplicationSettingsWidget))),
+ []
+ )
+ ).name;
diff --git a/app/portainer/react/components/index.ts b/app/portainer/react/components/index.ts
index b80f456c4..2730fc890 100644
--- a/app/portainer/react/components/index.ts
+++ b/app/portainer/react/components/index.ts
@@ -45,6 +45,7 @@ import { accessControlModule } from './access-control';
import { environmentsModule } from './environments';
import { envListModule } from './environments-list-view-components';
import { registriesModule } from './registries';
+import { accountModule } from './account';
export const ngModule = angular
.module('portainer.app.react.components', [
@@ -55,6 +56,7 @@ export const ngModule = angular
gitFormModule,
registriesModule,
settingsModule,
+ accountModule,
])
.component(
'tagSelector',
diff --git a/app/portainer/services/axios.ts b/app/portainer/services/axios.ts
index 73e3efeab..4849970ce 100644
--- a/app/portainer/services/axios.ts
+++ b/app/portainer/services/axios.ts
@@ -1,16 +1,53 @@
import axiosOrigin, { AxiosError, AxiosRequestConfig } from 'axios';
+import { setupCache } from 'axios-cache-adapter';
import { loadProgressBar } from 'axios-progress-bar';
import 'axios-progress-bar/dist/nprogress.css';
import PortainerError from '@/portainer/error';
import {
+ CACHE_DURATION,
+ dispatchCacheRefreshEventIfNeeded,
portainerAgentManagerOperation,
portainerAgentTargetHeader,
} from './http-request.helper';
+export const cache = setupCache({
+ maxAge: CACHE_DURATION,
+ debug: false, // set to true to print cache hits/misses
+ exclude: {
+ query: false, // include urls with query params
+ methods: ['put', 'patch', 'delete'],
+ filter: (req: AxiosRequestConfig) => {
+ // exclude caching get requests unless the path contains 'kubernetes'
+ if (!req.url?.includes('kubernetes') && req.method === 'get') {
+ return true;
+ }
+
+ // exclude caching post requests unless the path contains 'selfsubjectaccessreview'
+ if (
+ !req.url?.includes('selfsubjectaccessreview') &&
+ req.method === 'post'
+ ) {
+ return true;
+ }
+ return false;
+ },
+ },
+ // ask to clear cache on mutation
+ invalidate: async (_, req) => {
+ dispatchCacheRefreshEventIfNeeded(req);
+ },
+});
+
+// by default don't use the cache adapter
const axios = axiosOrigin.create({ baseURL: 'api' });
+// when entering a kubernetes environment, or updating user settings, update the cache adapter
+export function updateAxiosAdapter(useCache: boolean) {
+ axios.defaults.adapter = useCache ? cache.adapter : undefined;
+}
+
loadProgressBar(undefined, axios);
export default axios;
diff --git a/app/portainer/services/http-request.helper.ts b/app/portainer/services/http-request.helper.ts
index 3c0c7552a..19e244ccf 100644
--- a/app/portainer/services/http-request.helper.ts
+++ b/app/portainer/services/http-request.helper.ts
@@ -1,3 +1,31 @@
+import { AxiosRequestConfig } from 'axios';
+
+export const CACHE_DURATION = 5 * 60 * 1000; // 5m in ms
+// event emitted when cache need to be refreshed
+// used to sync $http + axios cache clear
+export const CACHE_REFRESH_EVENT = '__cache__refresh__event__';
+
+// utility function to dispatch catch refresh event
+export function dispatchCacheRefreshEvent() {
+ dispatchEvent(new CustomEvent(CACHE_REFRESH_EVENT, {}));
+}
+
+// perform checks on config.method and config.url
+// to dispatch event in only specific scenarios
+export function dispatchCacheRefreshEventIfNeeded(req: AxiosRequestConfig) {
+ if (
+ req.method &&
+ ['post', 'patch', 'put', 'delete'].includes(req.method.toLowerCase()) &&
+ // don't clear cache when we try to check for namespaces accesses
+ // otherwise we will clear it on every page
+ req.url &&
+ !req.url.includes('selfsubjectaccessreviews') &&
+ req.url.includes('kubernetes')
+ ) {
+ dispatchCacheRefreshEvent();
+ }
+}
+
interface Headers {
agentTargetQueue: string[];
agentManagerOperation: boolean;
diff --git a/app/portainer/users/queries/queryKeys.ts b/app/portainer/users/queries/queryKeys.ts
index 38a56fb7c..a31f07f08 100644
--- a/app/portainer/users/queries/queryKeys.ts
+++ b/app/portainer/users/queries/queryKeys.ts
@@ -1,7 +1,7 @@
import { UserId } from '../types';
-export const queryKeys = {
+export const userQueryKeys = {
base: () => ['users'] as const,
- user: (id: UserId) => [...queryKeys.base(), id] as const,
- me: () => [...queryKeys.base(), 'me'] as const,
+ user: (id: UserId) => [...userQueryKeys.base(), id] as const,
+ me: () => [...userQueryKeys.base(), 'me'] as const,
};
diff --git a/app/portainer/users/queries/useLoadCurrentUser.ts b/app/portainer/users/queries/useLoadCurrentUser.ts
index bb68dd8b8..e3cce9d3d 100644
--- a/app/portainer/users/queries/useLoadCurrentUser.ts
+++ b/app/portainer/users/queries/useLoadCurrentUser.ts
@@ -6,14 +6,14 @@ import { withError } from '@/react-tools/react-query';
import { buildUrl } from '../user.service';
import { User } from '../types';
-import { queryKeys } from './queryKeys';
+import { userQueryKeys } from './queryKeys';
interface CurrentUserResponse extends User {
forceChangePassword: boolean;
}
export function useLoadCurrentUser({ staleTime }: { staleTime?: number } = {}) {
- return useQuery(queryKeys.me(), () => getCurrentUser(), {
+ return useQuery(userQueryKeys.me(), () => getCurrentUser(), {
...withError('Unable to retrieve user details'),
staleTime,
});
diff --git a/app/portainer/users/queries/useUser.ts b/app/portainer/users/queries/useUser.ts
index 8986270a6..c43a7310e 100644
--- a/app/portainer/users/queries/useUser.ts
+++ b/app/portainer/users/queries/useUser.ts
@@ -6,13 +6,13 @@ import { withError } from '@/react-tools/react-query';
import { buildUrl } from '../user.service';
import { User, UserId } from '../types';
-import { queryKeys } from './queryKeys';
+import { userQueryKeys } from './queryKeys';
export function useUser(
id: UserId,
{ staleTime }: { staleTime?: number } = {}
) {
- return useQuery(queryKeys.user(id), () => getUser(id), {
+ return useQuery(userQueryKeys.user(id), () => getUser(id), {
...withError('Unable to retrieve user details'),
staleTime,
});
diff --git a/app/portainer/users/types.ts b/app/portainer/users/types.ts
index 93bbdb9e8..b505bbcc0 100644
--- a/app/portainer/users/types.ts
+++ b/app/portainer/users/types.ts
@@ -20,6 +20,7 @@ export type User = {
EndpointAuthorizations: {
[endpointId: EnvironmentId]: AuthorizationMap;
};
+ UseCache: boolean;
ThemeSettings: {
color: 'dark' | 'light' | 'highcontrast' | 'auto';
};
diff --git a/app/portainer/views/account/account.html b/app/portainer/views/account/account.html
index e19374908..589c34ad9 100644
--- a/app/portainer/views/account/account.html
+++ b/app/portainer/views/account/account.html
@@ -1,4 +1,4 @@
-
+
@@ -16,16 +16,16 @@