feat(cache): introduce cache option [EE-6293] (#10672)

Co-authored-by: testa113 <testa113>
pull/10675/head
Ali 1 year ago committed by GitHub
parent 57ed6ae6a6
commit 4096bb562d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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
}

@ -228,9 +228,9 @@ func (m *Migrator) initMigrations() {
m.migrateDockerDesktopExtensionSetting,
m.updateEdgeStackStatusForDB100,
)
m.addMigrations("2.20",
m.updateAppTemplatesVersionForDB110,
m.setUserCacheForDB110,
)
// Add new migrations below...

@ -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\"}"
}
}

@ -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()

@ -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

@ -2,6 +2,7 @@ import { Terminal } from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit';
import { csrfInterceptor, csrfTokenReaderInterceptorAngular } from './portainer/services/csrf';
import { agentInterceptor } from './portainer/services/axios';
import { dispatchCacheRefreshEventIfNeeded } from './portainer/services/http-request.helper';
/* @ngInject */
export function configApp($urlRouterProvider, $httpProvider, localStorageServiceProvider, $uibTooltipProvider, $compileProvider, cfpLoadingBarProvider) {
@ -9,6 +10,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');
$httpProvider.defaults.headers.post['Content-Type'] = 'application/json';

@ -1,4 +1,4 @@
<page-header title="'Create Edge stack'" breadcrumbs="[{label:'Edge Stacks', link:'edge.stacks'}, 'Create Edge stack']"> </page-header>
<page-header title="'Create Edge stack'" breadcrumbs="[{label:'Edge Stacks', link:'edge.stacks'}, 'Create Edge stack']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">

@ -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,

@ -1,4 +1,4 @@
<page-header title="'Create Custom template'" breadcrumbs="[{label:'Custom Templates', link:'kubernetes.templates.custom'}, 'Create Custom template']"> </page-header>
<page-header title="'Create Custom template'" breadcrumbs="[{label:'Custom Templates', link:'kubernetes.templates.custom'}, 'Create Custom template']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">

@ -1,4 +1,4 @@
<page-header title="'Registry access'" breadcrumbs="[{label:'Registries', link:'kubernetes.registries'}, $ctrl.registry.Name, 'Access management']"> </page-header>
<page-header title="'Registry access'" breadcrumbs="[{label:'Registries', link:'kubernetes.registries'}, $ctrl.registry.Name, 'Access management']" reload="true"> </page-header>
<registry-details registry="$ctrl.registry" ng-if="$ctrl.registry"></registry-details>

@ -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) => {

@ -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');
}

@ -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');

@ -12,6 +12,7 @@ export function UserViewModel(data) {
}
this.AuthenticationMethod = data.AuthenticationMethod;
this.Checked = false;
this.UseCache = data.UseCache;
}
export function UserTokenModel(data) {

@ -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;

@ -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',

@ -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;

@ -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;

@ -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,
};

@ -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,
});

@ -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,
});

@ -20,6 +20,7 @@ export type User = {
EndpointAuthorizations: {
[endpointId: EnvironmentId]: AuthorizationMap;
};
UseCache: boolean;
ThemeSettings: {
color: 'dark' | 'light' | 'highcontrast' | 'auto';
};

@ -1,4 +1,4 @@
<page-header title="'User settings'" breadcrumbs="['User settings']"> </page-header>
<page-header title="'User settings'" breadcrumbs="['User settings']" reload="true"> </page-header>
<demo-feature-indicator ng-if="isDemoUser" content="'You cannot change the password of this account in the demo version of Portainer.'"> </demo-feature-indicator>
@ -16,16 +16,16 @@
<form name="form" class="form-horizontal" style="margin-top: 15px">
<!-- current-password-input -->
<div class="form-group">
<label for="current_password" class="col-sm-2 control-label required text-left">Current password</label>
<div class="col-sm-8">
<label for="current_password" class="col-sm-3 col-lg-2 control-label required text-left">Current password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" ng-model="formValues.currentPassword" id="current_password" />
</div>
</div>
<!-- !current-password-input -->
<!-- new-password-input -->
<div class="form-group">
<label for="new_password" class="col-sm-2 control-label required text-left">New password</label>
<div class="col-sm-8">
<label for="new_password" class="col-sm-3 col-lg-2 control-label required text-left">New password</label>
<div class="col-sm-9 col-lg-10">
<input type="password" class="form-control" ng-model="formValues.newPassword" ng-minlength="requiredPasswordLength" id="new_password" name="new_password" />
</div>
</div>
@ -33,8 +33,8 @@
<!-- confirm-password-input -->
<div class="form-group">
<label for="confirm_password" class="col-sm-2 control-label required text-left">Confirm password</label>
<div class="col-sm-8">
<label for="confirm_password" class="col-sm-3 col-lg-2 control-label required text-left">Confirm password</label>
<div class="col-sm-9 col-lg-10">
<div class="input-group">
<input type="password" class="form-control" ng-model="formValues.confirmPassword" id="confirm_password" />
<span class="input-group-addon">
@ -86,6 +86,8 @@
</div>
</div>
<application-settings-widget></application-settings-widget>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">
<access-tokens-datatable

@ -1,4 +1,4 @@
<page-header title="'Create access token'" breadcrumbs="[{label:'User settings', link:'portainer.account'}, 'Add access token']"> </page-header>
<page-header title="'Create access token'" breadcrumbs="[{label:'User settings', link:'portainer.account'}, 'Add access token']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">

@ -1,4 +1,4 @@
<page-header title="'Create Custom template'" breadcrumbs="[{label:'Custom Templates', link:'docker.templates.custom'}, 'Create Custom template']"> </page-header>
<page-header title="'Create Custom template'" breadcrumbs="[{label:'Custom Templates', link:'docker.templates.custom'}, 'Create Custom template']" reload="true"> </page-header>
<div class="row" ng-if="!$ctrl.state.loading">
<div class="col-sm-12">

@ -1,4 +1,4 @@
<page-header title="'FDO Device Configuration'" breadcrumbs="[{label:'Environments', link:'portainer.endpoints'}, 'Import FDO Device']"> </page-header>
<page-header title="'FDO Device Configuration'" breadcrumbs="[{label:'Environments', link:'portainer.endpoints'}, 'Import FDO Device']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">

@ -1,4 +1,4 @@
<page-header title="'Create profile'" breadcrumbs="[{label:'Settings', link:'portainer.settings'}, 'Edge Compute']"> </page-header>
<page-header title="'Create profile'" breadcrumbs="[{label:'Settings', link:'portainer.settings'}, 'Edge Compute']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">

@ -1,4 +1,4 @@
<page-header title="'Edit profile'" breadcrumbs="[{label:'Settings', link:'portainer.settings'}, 'Edge Compute']"> </page-header>
<page-header title="'Edit profile'" breadcrumbs="[{label:'Settings', link:'portainer.settings'}, 'Edge Compute']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">

@ -7,6 +7,7 @@
link: 'portainer.endpoints.endpoint',
linkParams:{id: ctrl.endpoint.Id}
}, 'Access management']"
reload="true"
>
</page-header>

@ -9,6 +9,7 @@
},
$state.deviceName,
'KVM Control']"
reload="true"
>
</page-header>

@ -7,6 +7,7 @@
link: 'portainer.groups.group',
linkParams:{id: group.Id}
}, 'Access management']"
reload="true"
>
</page-header>

@ -1,4 +1,4 @@
<page-header title="'Create environment group'" breadcrumbs="[{label:'Environment groups', link:'portainer.groups'}, 'Add group']"> </page-header>
<page-header title="'Create environment group'" breadcrumbs="[{label:'Environment groups', link:'portainer.groups'}, 'Add group']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">

@ -1,4 +1,4 @@
<page-header title="'Environment group details'" breadcrumbs="[{label:'Groups', link:'portainer.groups'}, group.Name]"> </page-header>
<page-header title="'Environment group details'" breadcrumbs="[{label:'Groups', link:'portainer.groups'}, group.Name]" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">

@ -1,4 +1,4 @@
<page-header title="'Create registry'" breadcrumbs="[{label:'Registries', link:'portainer.registries'}, 'Add registry']"> </page-header>
<page-header title="'Create registry'" breadcrumbs="[{label:'Registries', link:'portainer.registries'}, 'Add registry']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">

@ -1,4 +1,4 @@
<page-header title="'Registry details'" breadcrumbs="[{label:'Registries', link:'portainer.registries'}, $ctrl.registry.Name]"> </page-header>
<page-header title="'Registry details'" breadcrumbs="[{label:'Registries', link:'portainer.registries'}, $ctrl.registry.Name]" reload="true"> </page-header>
<div class="row" ng-if="!$ctrl.state.loading">
<div class="col-sm-12">

@ -1,4 +1,4 @@
<page-header title="'Authentication settings'" breadcrumbs="[{label:'Settings', link:'portainer.settings'}, 'Authentication']"> </page-header>
<page-header title="'Authentication settings'" breadcrumbs="[{label:'Settings', link:'portainer.settings'}, 'Authentication']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">

@ -1,4 +1,4 @@
<page-header title="'Settings'" breadcrumbs="[{label:'Settings', link:'portainer.settings'}, 'Edge Compute']"> </page-header>
<page-header title="'Settings'" breadcrumbs="[{label:'Settings', link:'portainer.settings'}, 'Edge Compute']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12" ng-if="$ctrl.settings">

@ -1,4 +1,4 @@
<page-header title="'Create stack'" breadcrumbs="[{label:'Stacks', link:'docker.stacks'}, 'Add stack']"> </page-header>
<page-header title="'Create stack'" breadcrumbs="[{label:'Stacks', link:'docker.stacks'}, 'Add stack']" reload="true"> </page-header>
<div class="row">
<div class="col-sm-12">

@ -1,4 +1,4 @@
<page-header title="'User details'" breadcrumbs="[{label:'Users', link:'portainer.users'}, formValues.username]"> </page-header>
<page-header title="'User details'" breadcrumbs="[{label:'Users', link:'portainer.users'}, formValues.username]" reload="true"> </page-header>
<div class="row">
<div class="col-lg-12 col-md-12 col-xs-12">

@ -17,6 +17,7 @@ export function createMockUsers(
Checked: false,
EndpointAuthorizations: {},
PortainerAuthorizations: {},
UseCache: false,
ThemeSettings: {
color: 'auto',
},

@ -27,7 +27,7 @@ export function DashboardView() {
return (
<>
<PageHeader title="Home" breadcrumbs={[{ label: 'Dashboard' }]} />
<PageHeader title="Home" breadcrumbs={[{ label: 'Dashboard' }]} reload />
<div className="mx-4">
{subscriptionsQuery.data && (

@ -12,6 +12,7 @@ export function CreateView() {
{ link: 'azure.containerinstances', label: 'Container instances' },
{ label: 'Add container' },
]}
reload
/>
<div className="row">

@ -68,6 +68,7 @@ export function ItemView() {
{ link: 'azure.containerinstances', label: 'Container instances' },
{ label: container.name },
]}
reload
/>
<div className="row">

@ -32,9 +32,9 @@ export function ListView() {
return (
<>
<PageHeader
title="Container list"
breadcrumbs="Container instances"
reload
title="Container list"
/>
<ContainersDatatable

@ -31,6 +31,7 @@ function Template({ title }: StoryProps) {
{ label: 'bread3' },
{ label: 'bread4' },
]}
reload
/>
</UserContext.Provider>
);

@ -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();
}
}

@ -6,6 +6,7 @@ export function createMockUser(id: number, username: string): UserViewModel {
Username: username,
Role: 2,
EndpointAuthorizations: {},
UseCache: false,
PortainerAuthorizations: {
PortainerDockerHubInspect: true,
PortainerEndpointGroupInspect: true,

@ -36,6 +36,7 @@ export function CreateView() {
{ label: 'Containers', link: 'docker.containers' },
'Add container',
]}
reload
/>
<CreateForm />

@ -61,6 +61,7 @@ export function ItemView() {
label: networkQuery.data.Name,
},
]}
reload
/>
<NetworkDetailsTable
network={networkQuery.data}

@ -19,6 +19,7 @@ function WaitingRoomView() {
<PageHeader
title="Waiting Room"
breadcrumbs={[{ label: 'Waiting Room' }]}
reload
/>
<InformationPanel>

@ -27,7 +27,6 @@ export function ConfigureView() {
<>
<PageHeader
title="Kubernetes features configuration"
reload
breadcrumbs={[
{ label: 'Environments', link: 'portainer.endpoints' },
{
@ -37,6 +36,7 @@ export function ConfigureView() {
},
'Kubernetes configuration',
]}
reload
/>
<div className="row">
<div className="col-sm-12">

@ -573,6 +573,7 @@ export function CreateIngressView() {
label: isEdit ? 'Edit ingress' : 'Create ingress',
},
]}
reload
/>
<div className="row ingress-rules">
<div className="col-sm-12">

@ -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 (
<Formik<FormValues>
initialValues={initialValues}
onSubmit={handleSubmit}
validateOnMount
enableReinitialize
>
{({ isValid, dirty, values, setFieldValue }) => (
<Form className="form-horizontal">
<TextTip color="orange" className="mb-3">
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.
</TextTip>
<SwitchField
label="Enable front-end data caching for Kubernetes environments"
checked={values.useCache}
onChange={(value) => setFieldValue('useCache', value)}
labelClass="col-lg-2 col-sm-3" // match the label width of the other fields in the page
fieldClass="!mb-4"
/>
<div className="form-group">
<div className="col-sm-12">
<LoadingButton
loadingText="Saving..."
isLoading={updateSettingsMutation.isLoading}
disabled={!isValid || !dirty}
className="!ml-0"
data-cy="account-applicationSettingsSaveButton"
>
Save
</LoadingButton>
</div>
</div>
</Form>
)}
</Formik>
);
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'),
}
);
}
}

@ -0,0 +1,20 @@
import { Settings } from 'lucide-react';
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
import { ApplicationSettingsForm } from './ApplicationSettingsForm';
export function ApplicationSettingsWidget() {
return (
<div className="row">
<div className="col-sm-12">
<Widget>
<WidgetTitle icon={Settings} title="Application settings" />
<WidgetBody>
<ApplicationSettingsForm />
</WidgetBody>
</Widget>
</div>
</div>
);
}

@ -0,0 +1 @@
export { ApplicationSettingsWidget } from './ApplicationSettingsWidget';

@ -12,6 +12,7 @@ export function CreateHelmRepositoriesView() {
{ label: 'My account', link: 'portainer.account' },
{ label: 'Create Helm repository' },
]}
reload
/>
<div className="row">

@ -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<User>) => updateUser(user, userId),
mutationOptions(withInvalidate(queryClient, [userQueryKeys.base()]))
// error notification should be handled by the caller
);
}
async function updateUser(user: Partial<User>, userId: number) {
try {
const { data } = await axios.put(`/users/${userId}`, user);
return data;
} catch (error) {
throw parseAxiosError(error);
}
}

@ -17,6 +17,7 @@ function EdgeAutoCreateScriptView() {
{ label: 'Environments', link: 'portainer.endpoints' },
'Automatic Edge Environment Creation',
]}
reload
/>
<div className="mx-3">

@ -54,6 +54,7 @@ function CreateView() {
<PageHeader
title="Update & Rollback"
breadcrumbs="Edge agent update and rollback"
reload
/>
<BetaAlert

@ -78,6 +78,7 @@ function ItemView() {
{ label: 'Edge agent update and rollback', link: '^' },
item.name,
]}
reload
/>
<BetaAlert

@ -56,8 +56,8 @@ export function ListView() {
<>
<PageHeader
title="Update & Rollback"
reload
breadcrumbs="Update and rollback"
reload
/>
<BetaAlert

@ -27,6 +27,7 @@ export function EnvironmentTypeSelectView() {
<PageHeader
title="Quick Setup"
breadcrumbs={[{ label: 'Environment Wizard' }]}
reload
/>
<div className="row">

@ -71,6 +71,7 @@ export function EnvironmentCreationView() {
<PageHeader
title="Quick Setup"
breadcrumbs={[{ label: 'Environment Wizard' }]}
reload
/>
<div className={styles.wizardWrapper}>

@ -22,6 +22,7 @@ export function HomeView() {
<PageHeader
title="Quick Setup"
breadcrumbs={[{ label: 'Environment Wizard' }]}
reload
/>
<div className="row">

@ -18,7 +18,7 @@ import { ExperimentalFeatures } from './ExperimentalFeatures';
export function SettingsView() {
return (
<>
<PageHeader title="Settings" breadcrumbs="Settings" />
<PageHeader title="Settings" breadcrumbs="Settings" reload />
<div className="mx-4 space-y-4">
<ApplicationSettingsPanel onSuccess={handleSuccess} />

@ -38,6 +38,7 @@ export function ItemView() {
<PageHeader
title="Team details"
breadcrumbs={[{ label: 'Teams' }, { label: team.Name }]}
reload
/>
{membershipsQuery.data && (

@ -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,
},
];

@ -16,7 +16,11 @@ export function ListView() {
return (
<>
<PageHeader title="Teams" breadcrumbs={[{ label: 'Teams management' }]} />
<PageHeader
title="Teams"
breadcrumbs={[{ label: 'Teams management' }]}
reload
/>
{isAdmin && usersQuery.data && teamsQuery.data && (
<CreateTeamForm users={usersQuery.data} teams={teamsQuery.data} />

@ -8,6 +8,7 @@ const mockUser: User = {
Id: 1,
Role: 1,
Username: 'mock',
UseCache: false,
ThemeSettings: {
color: 'auto',
},

@ -74,6 +74,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",

@ -6470,6 +6470,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"
@ -7053,6 +7061,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"

Loading…
Cancel
Save