From 2c032f17399e79ae594de1a3fee8e7b2229bd574 Mon Sep 17 00:00:00 2001 From: Ali <83188384+testA113@users.noreply.github.com> Date: Mon, 20 Nov 2023 10:22:48 +1300 Subject: [PATCH] feat(cache): introduce cache option [EE-6293] (#10641) --- .../migrator/migrate_dbversion110.go | 18 ++++ api/datastore/migrator/migrator.go | 1 + .../test_data/output_24_to_latest.json | 4 +- api/http/handler/users/user_update.go | 5 ++ api/portainer.go | 7 +- app/config.js | 9 ++ .../create-edge-stack-view.html | 2 +- app/kubernetes/__module.js | 64 +++++++++++++- .../kube-create-custom-template-view.html | 2 +- .../kube-registry-access-view.html | 2 +- app/kubernetes/services/namespaceService.js | 10 ++- .../resource-pools/resourcePoolsController.js | 2 +- .../theme/theme-settings.controller.js | 4 +- app/portainer/models/user.js | 1 + app/portainer/react/components/account.ts | 17 ++++ app/portainer/react/components/index.ts | 2 + app/portainer/services/axios.ts | 37 ++++++++ app/portainer/services/http-request.helper.ts | 28 ++++++ app/portainer/users/queries/queryKeys.ts | 4 +- app/portainer/users/queries/useUser.ts | 4 +- app/portainer/users/types.ts | 1 + app/portainer/views/account/account.html | 16 ++-- .../create-user-access-token.html | 2 +- .../createCustomTemplateView.html | 2 +- .../views/devices/import/importDevice.html | 2 +- .../devices/profiles/add/addProfile.html | 2 +- .../devices/profiles/edit/editProfile.html | 2 +- .../endpoints/access/endpointAccess.html | 1 + .../views/endpoints/kvm/endpointKVM.html | 1 + .../views/groups/access/groupAccess.html | 1 + .../views/groups/create/creategroup.html | 2 +- app/portainer/views/groups/edit/group.html | 2 +- .../registries/create/createRegistry.html | 2 +- .../views/registries/edit/registry.html | 2 +- .../settingsAuthentication.html | 2 +- .../edge-compute/settingsEdgeCompute.html | 2 +- .../views/stacks/create/createstack.html | 2 +- app/portainer/views/users/edit/user.html | 2 +- app/react-tools/test-mocks.ts | 1 + .../azure/DashboardView/DashboardView.tsx | 2 +- .../CreateView/CreateView.tsx | 1 + .../container-instances/ItemView/ItemView.tsx | 1 + .../container-instances/ListView/ListView.tsx | 2 +- .../PageHeader/PageHeader.stories.tsx | 1 + .../components/PageHeader/PageHeader.tsx | 5 +- .../UsersSelector/UsersSelector.mocks.ts | 1 + .../containers/CreateView/CreateView.tsx | 1 + .../docker/networks/ItemView/ItemView.tsx | 1 + .../WaitingRoomView/WaitingRoomView.tsx | 1 + .../cluster/ConfigureView/ConfigureView.tsx | 2 +- .../CreateIngressView/CreateIngressView.tsx | 1 + .../ApplicationSettingsForm.tsx | 86 +++++++++++++++++++ .../ApplicationSettingsWidget.tsx | 20 +++++ .../AccountView/ApplicationSettings/index.ts | 1 + .../CreateHelmRepositoriesView.tsx | 1 + .../account/useUpdateUserMutation.tsx | 32 +++++++ .../EdgeAutoCreateScriptView.tsx | 1 + .../CreateView/CreateView.tsx | 1 + .../update-schedules/ItemView/ItemView.tsx | 1 + .../update-schedules/ListView/ListView.tsx | 2 +- .../EndpointTypeView.tsx | 1 + .../EnvironmentsCreationView.tsx | 1 + .../environments/wizard/HomeView/HomeView.tsx | 1 + .../settings/SettingsView/SettingsView.tsx | 2 +- .../users/teams/ItemView/ItemView.tsx | 1 + .../CreateTeamForm/CreateTeamForm.mocks.ts | 2 + .../users/teams/ListView/ListView.tsx | 6 +- app/react/test-utils/withUserProvider.tsx | 1 + package.json | 1 + yarn.lock | 13 +++ 70 files changed, 418 insertions(+), 45 deletions(-) create mode 100644 app/portainer/react/components/account.ts create mode 100644 app/react/portainer/account/AccountView/ApplicationSettings/ApplicationSettingsForm.tsx create mode 100644 app/react/portainer/account/AccountView/ApplicationSettings/ApplicationSettingsWidget.tsx create mode 100644 app/react/portainer/account/AccountView/ApplicationSettings/index.ts create mode 100644 app/react/portainer/account/useUpdateUserMutation.tsx diff --git a/api/datastore/migrator/migrate_dbversion110.go b/api/datastore/migrator/migrate_dbversion110.go index 88f0a4f19..d2bbcc677 100644 --- a/api/datastore/migrator/migrate_dbversion110.go +++ b/api/datastore/migrator/migrate_dbversion110.go @@ -23,3 +23,21 @@ func (migrator *Migrator) updateAppTemplatesVersionForDB110() error { return migrator.settingsService.UpdateSettings(settings) } + +// setUseCacheForDB110 sets the user cache to true for all users +func (migrator *Migrator) setUserCacheForDB110() error { + users, err := migrator.userService.ReadAll() + if err != nil { + return err + } + + for i := range users { + user := &users[i] + user.UseCache = true + if err := migrator.userService.Update(user.ID, user); err != nil { + return err + } + } + + return nil +} diff --git a/api/datastore/migrator/migrator.go b/api/datastore/migrator/migrator.go index 1e0f7253c..2ef3a0a68 100644 --- a/api/datastore/migrator/migrator.go +++ b/api/datastore/migrator/migrator.go @@ -231,6 +231,7 @@ func (m *Migrator) initMigrations() { m.addMigrations("2.20", m.updateAppTemplatesVersionForDB110, + m.setUserCacheForDB110, ) // Add new migrations below... diff --git a/api/datastore/test_data/output_24_to_latest.json b/api/datastore/test_data/output_24_to_latest.json index ccff02ba6..b91a5d4c5 100644 --- a/api/datastore/test_data/output_24_to_latest.json +++ b/api/datastore/test_data/output_24_to_latest.json @@ -903,6 +903,7 @@ "color": "" }, "TokenIssueAt": 0, + "UseCache": true, "Username": "admin" }, { @@ -932,10 +933,11 @@ "color": "" }, "TokenIssueAt": 0, + "UseCache": true, "Username": "prabhat" } ], "version": { - "VERSION": "{\"SchemaVersion\":\"2.20.0\",\"MigratorCount\":1,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" + "VERSION": "{\"SchemaVersion\":\"2.20.0\",\"MigratorCount\":2,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}" } } \ No newline at end of file diff --git a/api/http/handler/users/user_update.go b/api/http/handler/users/user_update.go index 7f10194e0..d963ef68b 100644 --- a/api/http/handler/users/user_update.go +++ b/api/http/handler/users/user_update.go @@ -24,6 +24,7 @@ type userUpdatePayload struct { Username string `validate:"required" example:"bob"` Password string `validate:"required" example:"cg9Wgky3"` NewPassword string `validate:"required" example:"asfj2emv"` + UseCache *bool `validate:"required" example:"true"` Theme *themePayload // User role (1 for administrator account and 2 for regular account) @@ -147,6 +148,10 @@ func (handler *Handler) userUpdate(w http.ResponseWriter, r *http.Request) *http } } + if payload.UseCache != nil { + user.UseCache = *payload.UseCache + } + if payload.Role != 0 { user.Role = portainer.UserRole(payload.Role) user.TokenIssueAt = time.Now().Unix() diff --git a/api/portainer.go b/api/portainer.go index 027a572cf..eafc6959d 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -1314,9 +1314,10 @@ type ( Username string `json:"Username" example:"bob"` Password string `json:"Password,omitempty" swaggerignore:"true"` // User role (1 for administrator account and 2 for regular account) - Role UserRole `json:"Role" example:"1"` - TokenIssueAt int64 `json:"TokenIssueAt" example:"1"` - ThemeSettings UserThemeSettings + Role UserRole `json:"Role" example:"1"` + TokenIssueAt int64 `json:"TokenIssueAt" example:"1"` + ThemeSettings UserThemeSettings `json:"ThemeSettings"` + UseCache bool `json:"UseCache" example:"true"` // Deprecated fields diff --git a/app/config.js b/app/config.js index fc72f3d22..9998b5953 100644 --- a/app/config.js +++ b/app/config.js @@ -1,6 +1,7 @@ import { Terminal } from 'xterm'; import * as fit from 'xterm/lib/addons/fit/fit'; import { agentInterceptor } from './portainer/services/axios'; +import { dispatchCacheRefreshEventIfNeeded } from './portainer/services/http-request.helper'; /* @ngInject */ export function configApp($urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, $uibTooltipProvider, $compileProvider, cfpLoadingBarProvider) { @@ -8,6 +9,14 @@ export function configApp($urlRouterProvider, $httpProvider, localStorageService $compileProvider.debugInfoEnabled(false); } + // ask to clear cache on mutation + $httpProvider.interceptors.push(() => ({ + request: (reqConfig) => { + dispatchCacheRefreshEventIfNeeded(reqConfig); + return reqConfig; + }, + })); + localStorageServiceProvider.setPrefix('portainer'); jwtOptionsProvider.config({ diff --git a/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.html b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.html index a2bf5914e..de7d96b08 100644 --- a/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.html +++ b/app/edge/views/edge-stacks/createEdgeStackView/create-edge-stack-view.html @@ -1,4 +1,4 @@ - +
diff --git a/app/kubernetes/__module.js b/app/kubernetes/__module.js index 234c1c594..39168d2ea 100644 --- a/app/kubernetes/__module.js +++ b/app/kubernetes/__module.js @@ -1,13 +1,52 @@ import { EnvironmentStatus } from '@/react/portainer/environments/types'; import { getSelfSubjectAccessReview } from '@/react/kubernetes/namespaces/getSelfSubjectAccessReview'; +import { updateAxiosAdapter } from '@/portainer/services/axios'; import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; +import { CACHE_REFRESH_EVENT, CACHE_DURATION } from '../portainer/services/http-request.helper'; +import { cache } from '../portainer/services/axios'; import registriesModule from './registries'; import customTemplateModule from './custom-templates'; import { reactModule } from './react'; import './views/kubernetes.css'; +// The angular-cache npm package didn't have exclude options, so implement a custom cache +// with an added check to only cache kubernetes requests +class ExpirationCache { + constructor() { + this.store = new Map(); + this.timeout = CACHE_DURATION; + } + + get(key) { + return this.store.get(key); + } + + put(key, val) { + // only cache requests with 'kubernetes' in the url + if (key.includes('kubernetes')) { + this.store.set(key, val); + // remove it once it's expired + setTimeout(() => { + this.remove(key); + }, this.timeout); + } + } + + remove(key) { + this.store.delete(key); + } + + removeAll() { + this.store = new Map(); + } + + delete() { + // skip because this is standalone, not a part of $cacheFactory + } +} + angular.module('portainer.kubernetes', ['portainer.app', registriesModule, customTemplateModule, reactModule]).config([ '$stateRegistryProvider', function ($stateRegistryProvider) { @@ -19,8 +58,31 @@ angular.module('portainer.kubernetes', ['portainer.app', registriesModule, custo parent: 'endpoint', abstract: true, - onEnter: /* @ngInject */ function onEnter($async, $state, endpoint, KubernetesHealthService, KubernetesNamespaceService, Notifications, StateManager) { + onEnter: /* @ngInject */ function onEnter( + $async, + $state, + endpoint, + KubernetesHealthService, + KubernetesNamespaceService, + Notifications, + StateManager, + $http, + Authentication, + UserService + ) { return $async(async () => { + // if the user wants to use front end cache for performance, set the angular caching settings + const userDetails = Authentication.getUserDetails(); + const user = await UserService.user(userDetails.ID); + updateAxiosAdapter(user.UseCache); + if (user.UseCache) { + $http.defaults.cache = new ExpirationCache(); + window.addEventListener(CACHE_REFRESH_EVENT, () => { + $http.defaults.cache.removeAll(); + cache.store.clear(); + }); + } + const kubeTypes = [ PortainerEndpointTypes.KubernetesLocalEnvironment, PortainerEndpointTypes.AgentOnKubernetesEnvironment, diff --git a/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.html b/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.html index fdfaab333..f00ce4d06 100644 --- a/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.html +++ b/app/kubernetes/custom-templates/kube-create-custom-template-view/kube-create-custom-template-view.html @@ -1,4 +1,4 @@ - +
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 d2648ade2..62f6f0022 100644 --- a/app/portainer/services/axios.ts +++ b/app/portainer/services/axios.ts @@ -1,4 +1,5 @@ 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'; @@ -6,12 +7,48 @@ import PortainerError from '@/portainer/error'; import { get as localStorageGet } from '@/react/hooks/useLocalStorage'; 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 c6f335d98..ec14e11c7 100644 --- a/app/portainer/users/queries/queryKeys.ts +++ b/app/portainer/users/queries/queryKeys.ts @@ -1,6 +1,6 @@ import { UserId } from '../types'; -export const queryKeys = { +export const userQueryKeys = { base: () => ['users'] as const, - user: (id: UserId) => [...queryKeys.base(), id] as const, + user: (id: UserId) => [...userQueryKeys.base(), id] as const, }; 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 @@
- -
+ +
- -
+ +
@@ -33,8 +33,8 @@
- -
+ +
@@ -86,6 +86,8 @@
+ +
+
diff --git a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html index c1a61e82e..ac36391b5 100644 --- a/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html +++ b/app/portainer/views/custom-templates/create-custom-template-view/createCustomTemplateView.html @@ -1,4 +1,4 @@ - +
diff --git a/app/portainer/views/devices/import/importDevice.html b/app/portainer/views/devices/import/importDevice.html index 4cb69ff8a..acfd85f35 100644 --- a/app/portainer/views/devices/import/importDevice.html +++ b/app/portainer/views/devices/import/importDevice.html @@ -1,4 +1,4 @@ - +
diff --git a/app/portainer/views/devices/profiles/add/addProfile.html b/app/portainer/views/devices/profiles/add/addProfile.html index a84f0be8b..4b64b269c 100644 --- a/app/portainer/views/devices/profiles/add/addProfile.html +++ b/app/portainer/views/devices/profiles/add/addProfile.html @@ -1,4 +1,4 @@ - +
diff --git a/app/portainer/views/devices/profiles/edit/editProfile.html b/app/portainer/views/devices/profiles/edit/editProfile.html index 976827b60..e3845f3d3 100644 --- a/app/portainer/views/devices/profiles/edit/editProfile.html +++ b/app/portainer/views/devices/profiles/edit/editProfile.html @@ -1,4 +1,4 @@ - +
diff --git a/app/portainer/views/endpoints/access/endpointAccess.html b/app/portainer/views/endpoints/access/endpointAccess.html index 0d59c2af8..47027f2d9 100644 --- a/app/portainer/views/endpoints/access/endpointAccess.html +++ b/app/portainer/views/endpoints/access/endpointAccess.html @@ -7,6 +7,7 @@ link: 'portainer.endpoints.endpoint', linkParams:{id: ctrl.endpoint.Id} }, 'Access management']" + reload="true" > diff --git a/app/portainer/views/endpoints/kvm/endpointKVM.html b/app/portainer/views/endpoints/kvm/endpointKVM.html index 5700d2942..25f91ae1c 100644 --- a/app/portainer/views/endpoints/kvm/endpointKVM.html +++ b/app/portainer/views/endpoints/kvm/endpointKVM.html @@ -9,6 +9,7 @@ }, $state.deviceName, 'KVM Control']" + reload="true" > diff --git a/app/portainer/views/groups/access/groupAccess.html b/app/portainer/views/groups/access/groupAccess.html index 0033fe5d1..6e11c9ba0 100644 --- a/app/portainer/views/groups/access/groupAccess.html +++ b/app/portainer/views/groups/access/groupAccess.html @@ -7,6 +7,7 @@ link: 'portainer.groups.group', linkParams:{id: group.Id} }, 'Access management']" + reload="true" > diff --git a/app/portainer/views/groups/create/creategroup.html b/app/portainer/views/groups/create/creategroup.html index 35770440f..562ecf550 100644 --- a/app/portainer/views/groups/create/creategroup.html +++ b/app/portainer/views/groups/create/creategroup.html @@ -1,4 +1,4 @@ - +
diff --git a/app/portainer/views/groups/edit/group.html b/app/portainer/views/groups/edit/group.html index f88b92d7b..c98d0e02d 100644 --- a/app/portainer/views/groups/edit/group.html +++ b/app/portainer/views/groups/edit/group.html @@ -1,4 +1,4 @@ - +
diff --git a/app/portainer/views/registries/create/createRegistry.html b/app/portainer/views/registries/create/createRegistry.html index 56c487a9b..719941492 100644 --- a/app/portainer/views/registries/create/createRegistry.html +++ b/app/portainer/views/registries/create/createRegistry.html @@ -1,4 +1,4 @@ - +
diff --git a/app/portainer/views/registries/edit/registry.html b/app/portainer/views/registries/edit/registry.html index 4f7f64e4b..026a32173 100644 --- a/app/portainer/views/registries/edit/registry.html +++ b/app/portainer/views/registries/edit/registry.html @@ -1,4 +1,4 @@ - +
diff --git a/app/portainer/views/settings/authentication/settingsAuthentication.html b/app/portainer/views/settings/authentication/settingsAuthentication.html index 08a5395ed..60e042657 100644 --- a/app/portainer/views/settings/authentication/settingsAuthentication.html +++ b/app/portainer/views/settings/authentication/settingsAuthentication.html @@ -1,4 +1,4 @@ - +
diff --git a/app/portainer/views/settings/edge-compute/settingsEdgeCompute.html b/app/portainer/views/settings/edge-compute/settingsEdgeCompute.html index be3fe488d..028621d41 100644 --- a/app/portainer/views/settings/edge-compute/settingsEdgeCompute.html +++ b/app/portainer/views/settings/edge-compute/settingsEdgeCompute.html @@ -1,4 +1,4 @@ - +
diff --git a/app/portainer/views/stacks/create/createstack.html b/app/portainer/views/stacks/create/createstack.html index 41ddb57e0..d925bf5b9 100644 --- a/app/portainer/views/stacks/create/createstack.html +++ b/app/portainer/views/stacks/create/createstack.html @@ -1,4 +1,4 @@ - +
diff --git a/app/portainer/views/users/edit/user.html b/app/portainer/views/users/edit/user.html index 9e4b17ac4..b51c9be65 100644 --- a/app/portainer/views/users/edit/user.html +++ b/app/portainer/views/users/edit/user.html @@ -1,4 +1,4 @@ - +
diff --git a/app/react-tools/test-mocks.ts b/app/react-tools/test-mocks.ts index 895f84e28..e20bf19e9 100644 --- a/app/react-tools/test-mocks.ts +++ b/app/react-tools/test-mocks.ts @@ -17,6 +17,7 @@ export function createMockUsers( Checked: false, EndpointAuthorizations: {}, PortainerAuthorizations: {}, + UseCache: false, ThemeSettings: { color: 'auto', }, diff --git a/app/react/azure/DashboardView/DashboardView.tsx b/app/react/azure/DashboardView/DashboardView.tsx index 6815e9ae3..56d561427 100644 --- a/app/react/azure/DashboardView/DashboardView.tsx +++ b/app/react/azure/DashboardView/DashboardView.tsx @@ -27,7 +27,7 @@ export function DashboardView() { return ( <> - +
{subscriptionsQuery.data && ( diff --git a/app/react/azure/container-instances/CreateView/CreateView.tsx b/app/react/azure/container-instances/CreateView/CreateView.tsx index a790d48f1..be1d66af5 100644 --- a/app/react/azure/container-instances/CreateView/CreateView.tsx +++ b/app/react/azure/container-instances/CreateView/CreateView.tsx @@ -12,6 +12,7 @@ export function CreateView() { { link: 'azure.containerinstances', label: 'Container instances' }, { label: 'Add container' }, ]} + reload />
diff --git a/app/react/azure/container-instances/ItemView/ItemView.tsx b/app/react/azure/container-instances/ItemView/ItemView.tsx index 54aeecf05..3a9e52394 100644 --- a/app/react/azure/container-instances/ItemView/ItemView.tsx +++ b/app/react/azure/container-instances/ItemView/ItemView.tsx @@ -68,6 +68,7 @@ export function ItemView() { { link: 'azure.containerinstances', label: 'Container instances' }, { label: container.name }, ]} + reload />
diff --git a/app/react/azure/container-instances/ListView/ListView.tsx b/app/react/azure/container-instances/ListView/ListView.tsx index 4c24acddb..27210f281 100644 --- a/app/react/azure/container-instances/ListView/ListView.tsx +++ b/app/react/azure/container-instances/ListView/ListView.tsx @@ -32,9 +32,9 @@ export function ListView() { return ( <> ); diff --git a/app/react/components/PageHeader/PageHeader.tsx b/app/react/components/PageHeader/PageHeader.tsx index 69e24c2f9..62e11fcf7 100644 --- a/app/react/components/PageHeader/PageHeader.tsx +++ b/app/react/components/PageHeader/PageHeader.tsx @@ -1,6 +1,8 @@ import { useRouter } from '@uirouter/react'; -import { RefreshCw } from 'lucide-react'; import { PropsWithChildren } from 'react'; +import { RefreshCw } from 'lucide-react'; + +import { dispatchCacheRefreshEvent } from '@/portainer/services/http-request.helper'; import { Button } from '../buttons'; @@ -51,6 +53,7 @@ export function PageHeader({ ); function onClickedRefresh() { + dispatchCacheRefreshEvent(); return onReload ? onReload() : router.stateService.reload(); } } diff --git a/app/react/components/UsersSelector/UsersSelector.mocks.ts b/app/react/components/UsersSelector/UsersSelector.mocks.ts index a245054e3..d7ae9abbc 100644 --- a/app/react/components/UsersSelector/UsersSelector.mocks.ts +++ b/app/react/components/UsersSelector/UsersSelector.mocks.ts @@ -6,6 +6,7 @@ export function createMockUser(id: number, username: string): UserViewModel { Username: username, Role: 2, EndpointAuthorizations: {}, + UseCache: false, PortainerAuthorizations: { PortainerDockerHubInspect: true, PortainerEndpointGroupInspect: true, diff --git a/app/react/docker/containers/CreateView/CreateView.tsx b/app/react/docker/containers/CreateView/CreateView.tsx index 2502812f1..bec91a0ef 100644 --- a/app/react/docker/containers/CreateView/CreateView.tsx +++ b/app/react/docker/containers/CreateView/CreateView.tsx @@ -36,6 +36,7 @@ export function CreateView() { { label: 'Containers', link: 'docker.containers' }, 'Add container', ]} + reload /> diff --git a/app/react/docker/networks/ItemView/ItemView.tsx b/app/react/docker/networks/ItemView/ItemView.tsx index b6131e037..a7c396d14 100644 --- a/app/react/docker/networks/ItemView/ItemView.tsx +++ b/app/react/docker/networks/ItemView/ItemView.tsx @@ -61,6 +61,7 @@ export function ItemView() { label: networkQuery.data.Name, }, ]} + reload /> diff --git a/app/react/kubernetes/cluster/ConfigureView/ConfigureView.tsx b/app/react/kubernetes/cluster/ConfigureView/ConfigureView.tsx index 8aef84f71..b5d24ed8c 100644 --- a/app/react/kubernetes/cluster/ConfigureView/ConfigureView.tsx +++ b/app/react/kubernetes/cluster/ConfigureView/ConfigureView.tsx @@ -27,7 +27,6 @@ export function ConfigureView() { <>
diff --git a/app/react/kubernetes/ingresses/CreateIngressView/CreateIngressView.tsx b/app/react/kubernetes/ingresses/CreateIngressView/CreateIngressView.tsx index dbb34884d..0d686b807 100644 --- a/app/react/kubernetes/ingresses/CreateIngressView/CreateIngressView.tsx +++ b/app/react/kubernetes/ingresses/CreateIngressView/CreateIngressView.tsx @@ -573,6 +573,7 @@ export function CreateIngressView() { label: isEdit ? 'Edit ingress' : 'Create ingress', }, ]} + reload />
diff --git a/app/react/portainer/account/AccountView/ApplicationSettings/ApplicationSettingsForm.tsx b/app/react/portainer/account/AccountView/ApplicationSettings/ApplicationSettingsForm.tsx new file mode 100644 index 000000000..c0e33d4a6 --- /dev/null +++ b/app/react/portainer/account/AccountView/ApplicationSettings/ApplicationSettingsForm.tsx @@ -0,0 +1,86 @@ +import { Form, Formik } from 'formik'; + +import { useCurrentUser } from '@/react/hooks/useUser'; +import { notifySuccess } from '@/portainer/services/notifications'; +import { updateAxiosAdapter } from '@/portainer/services/axios'; +import { withError } from '@/react-tools/react-query'; + +import { TextTip } from '@@/Tip/TextTip'; +import { LoadingButton } from '@@/buttons'; +import { SwitchField } from '@@/form-components/SwitchField'; + +import { useUpdateUserMutation } from '../../useUpdateUserMutation'; + +type FormValues = { + useCache: boolean; +}; + +export function ApplicationSettingsForm() { + const { user } = useCurrentUser(); + const updateSettingsMutation = useUpdateUserMutation(); + + const initialValues = { + useCache: user.UseCache, + }; + + return ( + + initialValues={initialValues} + onSubmit={handleSubmit} + validateOnMount + enableReinitialize + > + {({ isValid, dirty, values, setFieldValue }) => ( + + + Enabling front-end data caching can mean that changes to Kubernetes + clusters made by other users or outside of Portainer may take up to + five minutes to show in your session. This caching only applies to + Kubernetes environments. + + setFieldValue('useCache', value)} + labelClass="col-lg-2 col-sm-3" // match the label width of the other fields in the page + fieldClass="!mb-4" + /> +
+
+ + Save + +
+
+ + )} + + ); + + function handleSubmit(values: FormValues) { + updateSettingsMutation.mutate( + { + Id: user.Id, + UseCache: values.useCache, + }, + { + onSuccess() { + updateAxiosAdapter(values.useCache); + notifySuccess( + 'Success', + 'Successfully updated application settings.' + ); + // a full reload is required to update the angular $http cache setting + setTimeout(() => window.location.reload(), 2000); // allow 2s to show the success notification + }, + ...withError('Unable to update application settings'), + } + ); + } +} diff --git a/app/react/portainer/account/AccountView/ApplicationSettings/ApplicationSettingsWidget.tsx b/app/react/portainer/account/AccountView/ApplicationSettings/ApplicationSettingsWidget.tsx new file mode 100644 index 000000000..31ff5eabe --- /dev/null +++ b/app/react/portainer/account/AccountView/ApplicationSettings/ApplicationSettingsWidget.tsx @@ -0,0 +1,20 @@ +import { Settings } from 'lucide-react'; + +import { Widget, WidgetBody, WidgetTitle } from '@@/Widget'; + +import { ApplicationSettingsForm } from './ApplicationSettingsForm'; + +export function ApplicationSettingsWidget() { + return ( +
+
+ + + + + + +
+
+ ); +} diff --git a/app/react/portainer/account/AccountView/ApplicationSettings/index.ts b/app/react/portainer/account/AccountView/ApplicationSettings/index.ts new file mode 100644 index 000000000..fd49063b6 --- /dev/null +++ b/app/react/portainer/account/AccountView/ApplicationSettings/index.ts @@ -0,0 +1 @@ +export { ApplicationSettingsWidget } from './ApplicationSettingsWidget'; diff --git a/app/react/portainer/account/helm-repositories/CreateHelmRepositoryView/CreateHelmRepositoriesView.tsx b/app/react/portainer/account/helm-repositories/CreateHelmRepositoryView/CreateHelmRepositoriesView.tsx index 88be91427..a5351badd 100644 --- a/app/react/portainer/account/helm-repositories/CreateHelmRepositoryView/CreateHelmRepositoriesView.tsx +++ b/app/react/portainer/account/helm-repositories/CreateHelmRepositoryView/CreateHelmRepositoriesView.tsx @@ -12,6 +12,7 @@ export function CreateHelmRepositoriesView() { { label: 'My account', link: 'portainer.account' }, { label: 'Create Helm repository' }, ]} + reload />
diff --git a/app/react/portainer/account/useUpdateUserMutation.tsx b/app/react/portainer/account/useUpdateUserMutation.tsx new file mode 100644 index 000000000..85a8edf9b --- /dev/null +++ b/app/react/portainer/account/useUpdateUserMutation.tsx @@ -0,0 +1,32 @@ +import { useMutation } from 'react-query'; + +import axios, { parseAxiosError } from '@/portainer/services/axios'; +import { User } from '@/portainer/users/types'; +import { + mutationOptions, + withInvalidate, + queryClient, +} from '@/react-tools/react-query'; +import { userQueryKeys } from '@/portainer/users/queries/queryKeys'; +import { useCurrentUser } from '@/react/hooks/useUser'; + +export function useUpdateUserMutation() { + const { + user: { Id: userId }, + } = useCurrentUser(); + + return useMutation( + (user: Partial) => updateUser(user, userId), + mutationOptions(withInvalidate(queryClient, [userQueryKeys.base()])) + // error notification should be handled by the caller + ); +} + +async function updateUser(user: Partial, userId: number) { + try { + const { data } = await axios.put(`/users/${userId}`, user); + return data; + } catch (error) { + throw parseAxiosError(error); + } +} diff --git a/app/react/portainer/environments/EdgeAutoCreateScriptView/EdgeAutoCreateScriptView.tsx b/app/react/portainer/environments/EdgeAutoCreateScriptView/EdgeAutoCreateScriptView.tsx index de6666511..87cd7c61c 100644 --- a/app/react/portainer/environments/EdgeAutoCreateScriptView/EdgeAutoCreateScriptView.tsx +++ b/app/react/portainer/environments/EdgeAutoCreateScriptView/EdgeAutoCreateScriptView.tsx @@ -17,6 +17,7 @@ function EdgeAutoCreateScriptView() { { label: 'Environments', link: 'portainer.endpoints' }, 'Automatic Edge Environment Creation', ]} + reload />
diff --git a/app/react/portainer/environments/update-schedules/CreateView/CreateView.tsx b/app/react/portainer/environments/update-schedules/CreateView/CreateView.tsx index bd809e35a..7c91c8010 100644 --- a/app/react/portainer/environments/update-schedules/CreateView/CreateView.tsx +++ b/app/react/portainer/environments/update-schedules/CreateView/CreateView.tsx @@ -54,6 +54,7 @@ function CreateView() {
diff --git a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx index e0a2f37c8..427d4eb44 100644 --- a/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx +++ b/app/react/portainer/environments/wizard/EnvironmentsCreationView/EnvironmentsCreationView.tsx @@ -71,6 +71,7 @@ export function EnvironmentCreationView() {
diff --git a/app/react/portainer/environments/wizard/HomeView/HomeView.tsx b/app/react/portainer/environments/wizard/HomeView/HomeView.tsx index 68d93b220..9069bc798 100644 --- a/app/react/portainer/environments/wizard/HomeView/HomeView.tsx +++ b/app/react/portainer/environments/wizard/HomeView/HomeView.tsx @@ -22,6 +22,7 @@ export function HomeView() {
diff --git a/app/react/portainer/settings/SettingsView/SettingsView.tsx b/app/react/portainer/settings/SettingsView/SettingsView.tsx index 00c7e40a8..22ce1b5a1 100644 --- a/app/react/portainer/settings/SettingsView/SettingsView.tsx +++ b/app/react/portainer/settings/SettingsView/SettingsView.tsx @@ -18,7 +18,7 @@ import { ExperimentalFeatures } from './ExperimentalFeatures'; export function SettingsView() { return ( <> - +
diff --git a/app/react/portainer/users/teams/ItemView/ItemView.tsx b/app/react/portainer/users/teams/ItemView/ItemView.tsx index 71221cc12..a0492b1de 100644 --- a/app/react/portainer/users/teams/ItemView/ItemView.tsx +++ b/app/react/portainer/users/teams/ItemView/ItemView.tsx @@ -38,6 +38,7 @@ export function ItemView() { {membershipsQuery.data && ( diff --git a/app/react/portainer/users/teams/ListView/CreateTeamForm/CreateTeamForm.mocks.ts b/app/react/portainer/users/teams/ListView/CreateTeamForm/CreateTeamForm.mocks.ts index aa599082d..9bd264000 100644 --- a/app/react/portainer/users/teams/ListView/CreateTeamForm/CreateTeamForm.mocks.ts +++ b/app/react/portainer/users/teams/ListView/CreateTeamForm/CreateTeamForm.mocks.ts @@ -42,6 +42,7 @@ export function mockExampleData() { RoleName: 'user', Checked: false, AuthenticationMethod: '', + UseCache: false, }, { Id: 13, @@ -69,6 +70,7 @@ export function mockExampleData() { RoleName: 'user', Checked: false, AuthenticationMethod: '', + UseCache: false, }, ]; diff --git a/app/react/portainer/users/teams/ListView/ListView.tsx b/app/react/portainer/users/teams/ListView/ListView.tsx index 01bc181bc..3c7e3de91 100644 --- a/app/react/portainer/users/teams/ListView/ListView.tsx +++ b/app/react/portainer/users/teams/ListView/ListView.tsx @@ -16,7 +16,11 @@ export function ListView() { return ( <> - + {isAdmin && usersQuery.data && teamsQuery.data && ( diff --git a/app/react/test-utils/withUserProvider.tsx b/app/react/test-utils/withUserProvider.tsx index c4e77c5df..3eaefdf3c 100644 --- a/app/react/test-utils/withUserProvider.tsx +++ b/app/react/test-utils/withUserProvider.tsx @@ -8,6 +8,7 @@ const mockUser: User = { Id: 1, Role: 1, Username: 'mock', + UseCache: false, ThemeSettings: { color: 'auto', }, diff --git a/package.json b/package.json index 40417fedc..41d933d4a 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ "angularjs-slider": "^6.4.0", "angulartics": "^1.6.0", "axios": "^0.24.0", + "axios-cache-adapter": "^2.7.3", "axios-progress-bar": "^1.2.0", "babel-plugin-angularjs-annotate": "^0.10.0", "bootstrap": "^3.4.0", diff --git a/yarn.lock b/yarn.lock index 855a9958a..beebb3b69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6475,6 +6475,14 @@ axe-core@^4.6.2: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== +axios-cache-adapter@^2.7.3: + version "2.7.3" + resolved "https://registry.yarnpkg.com/axios-cache-adapter/-/axios-cache-adapter-2.7.3.tgz#0d1eefa0f25b88f42a95c7528d7345bde688181d" + integrity sha512-A+ZKJ9lhpjthOEp4Z3QR/a9xC4du1ALaAsejgRGrH9ef6kSDxdFrhRpulqsh9khsEnwXxGfgpUuDp1YXMNMEiQ== + dependencies: + cache-control-esm "1.0.0" + md5 "^2.2.1" + axios-progress-bar@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/axios-progress-bar/-/axios-progress-bar-1.2.0.tgz#f9ee88dc9af977246be1ef07eedfa4c990c639c5" @@ -7058,6 +7066,11 @@ c8@^7.6.0: yargs "^16.2.0" yargs-parser "^20.2.9" +cache-control-esm@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cache-control-esm/-/cache-control-esm-1.0.0.tgz#417647ecf1837a5e74155f55d5a4ae32a84e2581" + integrity sha512-Fa3UV4+eIk4EOih8FTV6EEsVKO0W5XWtNs6FC3InTfVz+EjurjPfDXY5wZDo/lxjDxg5RjNcurLyxEJBcEUx9g== + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"