feat(auth): add useIsEdgeAdmin hook [EE-6627] (#11057)

pull/11188/head
Chaim Lev-Ari 2024-02-15 00:50:20 +02:00 committed by GitHub
parent 7a6c872948
commit 31f5b42962
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 389 additions and 198 deletions

View File

@ -64,7 +64,7 @@ angular.module('portainer.app').controller('porAccessControlFormController', [
this.$onInit = $onInit;
function $onInit() {
var isAdmin = Authentication.isAdmin();
var isAdmin = Authentication.isPureAdmin();
ctrl.isAdmin = isAdmin;
if (isAdmin) {

View File

@ -1,4 +1,5 @@
import { getCurrentUser } from '../users/queries/useLoadCurrentUser';
import * as userHelpers from '../users/user.helpers';
import { clear as clearSessionStorage } from './session-storage';
const DEFAULT_USER = 'admin';
@ -25,6 +26,9 @@ angular.module('portainer.app').factory('Authentication', [
service.isAuthenticated = isAuthenticated;
service.getUserDetails = getUserDetails;
service.isAdmin = isAdmin;
service.isEdgeAdmin = isEdgeAdmin;
service.isPureAdmin = isPureAdmin;
service.hasAuthorizations = hasAuthorizations;
async function initAsync() {
try {
@ -120,8 +124,36 @@ angular.module('portainer.app').factory('Authentication', [
return login(DEFAULT_USER, DEFAULT_PASSWORD);
}
// To avoid creating divergence between CE and EE
// isAdmin checks if the user is a portainer admin or edge admin
function isEdgeAdmin() {
const environment = EndpointProvider.currentEndpoint();
return userHelpers.isEdgeAdmin({ Role: user.role }, environment);
}
/**
* @deprecated use Authentication.isAdmin instead
*/
function isAdmin() {
return !!user && user.role === 1;
return isEdgeAdmin();
}
// To avoid creating divergence between CE and EE
// isPureAdmin checks if the user is portainer admin only
function isPureAdmin() {
return userHelpers.isPureAdmin({ Role: user.role });
}
function hasAuthorizations(authorizations) {
const endpointId = EndpointProvider.endpointID();
if (isAdmin()) {
return true;
}
if (!user.endpointAuthorizations || !user.endpointAuthorizations[endpointId]) {
return false;
}
const userEndpointAuthorizations = user.endpointAuthorizations[endpointId];
return authorizations.some((authorization) => userEndpointAuthorizations[authorization]);
}
if (process.env.NODE_ENV === 'development') {

View File

@ -1,9 +1,9 @@
import { useQuery } from 'react-query';
import { TeamRole, TeamMembership } from '@/react/portainer/users/teams/types';
import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { User, UserId } from './types';
import { isAdmin } from './user.helpers';
import { getUserMemberships, getUsers } from './user.service';
interface UseUserMembershipOptions<TSelect> {
@ -22,14 +22,21 @@ export function useUserMembership<TSelect = TeamMembership[]>(
);
}
export function useIsTeamLeader(user: User) {
export function useIsCurrentUserTeamLeader() {
const { user } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
const query = useUserMembership(user.Id, {
enabled: !isAdmin(user),
enabled: !isAdminQuery.isLoading && !isAdminQuery.isAdmin,
select: (memberships) =>
memberships.some((membership) => membership.Role === TeamRole.Leader),
});
return isAdmin(user) ? true : query.data;
if (isAdminQuery.isLoading) {
return false;
}
return isAdminQuery.isAdmin ? true : !!query.data;
}
export function useUsers<T = User[]>(

View File

@ -7,6 +7,7 @@ export { type UserId };
export enum Role {
Admin = 1,
Standard,
EdgeAdmin,
}
interface AuthorizationMap {

View File

@ -1,9 +1,30 @@
import { Environment } from '@/react/portainer/environments/types';
import { isEdgeEnvironment } from '@/react/portainer/environments/utils';
import { Role, User } from './types';
export function filterNonAdministratorUsers(users: User[]) {
return users.filter((user) => user.Role !== Role.Admin);
}
export function isAdmin(user?: User): boolean {
return !!user && user.Role === 1;
type UserLike = Pick<User, 'Role'>;
// To avoid creating divergence between CE and EE
// isAdmin checks if the user is portainer admin or edge admin
export function isEdgeAdmin(
user: UserLike | undefined,
environment?: Pick<Environment, 'Type'> | null
): boolean {
return (
isPureAdmin(user) ||
(user?.Role === Role.EdgeAdmin &&
(!environment || isEdgeEnvironment(environment.Type)))
);
}
// To avoid creating divergence between CE and EE
// isPureAdmin checks only if the user is portainer admin
// See bouncer.IsAdmin and bouncer.PureAdminAccess
export function isPureAdmin(user?: UserLike): boolean {
return !!user && user.Role === Role.Admin;
}

View File

@ -4,7 +4,7 @@ import { Plus } from 'lucide-react';
import { ContainerInstanceFormValues } from '@/react/azure/types';
import * as notifications from '@/portainer/services/notifications';
import { useUser } from '@/react/hooks/useUser';
import { useCurrentUser } from '@/react/hooks/useUser';
import { AccessControlForm } from '@/react/portainer/access-control/AccessControlForm';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
@ -24,7 +24,7 @@ import { useCreateInstanceMutation } from './useCreateInstanceMutation';
export function CreateContainerInstanceForm() {
const environmentId = useEnvironmentId();
const { isAdmin } = useUser();
const { isPureAdmin } = useCurrentUser();
const { providers, subscriptions, resourceGroups, isLoading } =
useLoadFormState(environmentId);
@ -49,7 +49,7 @@ export function CreateContainerInstanceForm() {
return (
<Formik<ContainerInstanceFormValues>
initialValues={initialValues}
validationSchema={() => validationSchema(isAdmin)}
validationSchema={() => validationSchema(isPureAdmin)}
onSubmit={onSubmit}
validateOnMount
validateOnChange

View File

@ -37,7 +37,7 @@ export function useFormState(
resourceGroups: Record<string, ResourceGroup[]> = {},
providers: Record<string, ProviderViewModel> = {}
) {
const { isAdmin, user } = useCurrentUser();
const { user, isPureAdmin } = useCurrentUser();
const subscriptionOptions = subscriptions.map((s) => ({
value: s.subscriptionId,
@ -67,7 +67,7 @@ export function useFormState(
cpu: 1,
ports: [{ container: 80, host: 80, protocol: 'TCP' }],
allocatePublicIP: true,
accessControl: parseAccessControlFormData(isAdmin, user.Id),
accessControl: parseAccessControlFormData(isPureAdmin, user.Id),
};
return {

View File

@ -66,7 +66,7 @@ function RateLimitsInner({
environment: Environment;
}) {
const pullRateLimits = useRateLimits(registryId, environment, onRateLimit);
const { isAdmin } = useCurrentUser();
const { isPureAdmin } = useCurrentUser();
if (!pullRateLimits) {
return null;
@ -88,7 +88,7 @@ function RateLimitsInner({
</>
) : (
<>
{isAdmin ? (
{isPureAdmin ? (
<>
You are currently using an anonymous account to pull images
from DockerHub and will be limited to 100 pulls every 6

View File

@ -10,7 +10,7 @@ import { Values } from './BaseForm';
export function toViewModel(
config: ContainerResponse,
isAdmin: boolean,
isPureAdmin: boolean,
currentUserId: UserId,
nodeName: string,
image: Values['image'],
@ -18,7 +18,7 @@ export function toViewModel(
): Values {
// accessControl shouldn't be copied to new container
const accessControl = parseAccessControlFormData(isAdmin, currentUserId);
const accessControl = parseAccessControlFormData(isPureAdmin, currentUserId);
if (config.Portainer?.ResourceControl?.Public) {
accessControl.ownership = ResourceControlOwnership.PUBLIC;
@ -38,11 +38,11 @@ export function toViewModel(
}
export function getDefaultViewModel(
isAdmin: boolean,
isPureAdmin: boolean,
currentUserId: UserId,
nodeName: string
): Values {
const accessControl = parseAccessControlFormData(isAdmin, currentUserId);
const accessControl = parseAccessControlFormData(isPureAdmin, currentUserId);
return {
nodeName,

View File

@ -2,7 +2,7 @@ import { Formik } from 'formik';
import { useRouter } from '@uirouter/react';
import { useEffect, useState } from 'react';
import { useCurrentUser, useIsEnvironmentAdmin } from '@/react/hooks/useUser';
import { useIsEdgeAdmin, useIsEnvironmentAdmin } from '@/react/hooks/useUser';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries';
@ -48,7 +48,7 @@ function CreateForm() {
const environmentId = useEnvironmentId();
const router = useRouter();
const { trackEvent } = useAnalytics();
const { isAdmin } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
const isEnvironmentAdmin = useIsEnvironmentAdmin();
const [isDockerhubRateLimited, setIsDockerhubRateLimited] = useState(false);
@ -67,7 +67,7 @@ function CreateForm() {
const envQuery = useCurrentEnvironment();
const validationSchema = useValidation({
isAdmin,
isAdmin: isAdminQuery.isAdmin,
maxCpu,
maxMemory,
isDuplicating: initialValuesQuery?.isDuplicating,

View File

@ -102,7 +102,7 @@ export function InnerForm({
}
errors={errors.volumes}
allowBindMounts={
isEnvironmentAdmin ||
isEnvironmentAdmin.authorized ||
environment.SecuritySettings
.allowBindMountsForRegularUsers
}
@ -166,18 +166,18 @@ export function InnerForm({
setFieldValue(`resources.${field}`, value)
}
allowPrivilegedMode={
isEnvironmentAdmin ||
isEnvironmentAdmin.authorized ||
environment.SecuritySettings
.allowPrivilegedModeForRegularUsers
}
isDevicesFieldVisible={
isEnvironmentAdmin ||
isEnvironmentAdmin.authorized ||
environment.SecuritySettings
.allowDeviceMappingForRegularUsers
}
isInitFieldVisible={apiVersion >= 1.37}
isSysctlFieldVisible={
isEnvironmentAdmin ||
isEnvironmentAdmin.authorized ||
environment.SecuritySettings
.allowSysctlSettingForRegularUsers
}

View File

@ -62,7 +62,8 @@ export function useInitialValues(submitting: boolean) {
params: { nodeName, from },
} = useCurrentStateAndParams();
const environmentId = useEnvironmentId();
const { isAdmin, user } = useCurrentUser();
const { user, isPureAdmin } = useCurrentUser();
const networksQuery = useNetworksForSelector();
const fromContainerQuery = useContainer(environmentId, from, {
@ -85,7 +86,7 @@ export function useInitialValues(submitting: boolean) {
if (!from) {
return {
initialValues: defaultValues(isAdmin, user.Id, nodeName),
initialValues: defaultValues(isPureAdmin, user.Id, nodeName),
};
}
@ -136,7 +137,7 @@ export function useInitialValues(submitting: boolean) {
env: envVarsTabUtils.toViewModel(fromContainer),
...baseFormUtils.toViewModel(
fromContainer,
isAdmin,
isPureAdmin,
user.Id,
nodeName,
imageConfig,
@ -148,7 +149,7 @@ export function useInitialValues(submitting: boolean) {
}
function defaultValues(
isAdmin: boolean,
isPureAdmin: boolean,
currentUserId: UserId,
nodeName: string
): Values {
@ -161,6 +162,6 @@ function defaultValues(
resources: resourcesTabUtils.getDefaultViewModel(),
capabilities: capabilitiesTabUtils.getDefaultViewModel(),
env: envVarsTabUtils.getDefaultViewModel(),
...baseFormUtils.getDefaultViewModel(isAdmin, currentUserId, nodeName),
...baseFormUtils.getDefaultViewModel(isPureAdmin, currentUserId, nodeName),
};
}

View File

@ -1,4 +1,4 @@
import { render } from '@/react-tools/test-utils';
import { renderWithQueryClient } from '@/react-tools/test-utils';
import { UserContext } from '@/react/hooks/useUser';
import { UserViewModel } from '@/portainer/models/user';
@ -50,7 +50,7 @@ test('Non system networks should have a delete button', async () => {
async function renderComponent(isAdmin: boolean, network: DockerNetwork) {
const user = new UserViewModel({ Username: 'test', Role: isAdmin ? 1 : 2 });
const queries = render(
const queries = renderWithQueryClient(
<UserContext.Provider value={{ user }}>
<NetworkDetailsTable
network={network}

View File

@ -1,7 +1,7 @@
import { Layers } from 'lucide-react';
import { Row } from '@tanstack/react-table';
import { useAuthorizations, useCurrentUser } from '@/react/hooks/useUser';
import { useAuthorizations, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { Datatable } from '@@/datatables';
@ -34,7 +34,7 @@ export function StacksDatatable({
}) {
const tableState = useTableState(settingsStore, tableKey);
useRepeater(tableState.autoRefreshRate, onReload);
const { isAdmin } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
const canManageStacks = useAuthorizations([
'PortainerStackCreate',
'PortainerStackDelete',
@ -58,7 +58,7 @@ export function StacksDatatable({
columns={columns}
dataset={dataset}
isRowSelectable={({ original: item }) =>
allowSelection(item, isAdmin, canManageStacks)
allowSelection(item, isAdminQuery.isAdmin, canManageStacks.authorized)
}
getRowId={(item) => item.Id.toString()}
initialTableState={{

View File

@ -1,6 +1,6 @@
import { CellContext, Column } from '@tanstack/react-table';
import { useCurrentUser } from '@/react/hooks/useUser';
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
import { getValueAsArrayOfStrings } from '@/portainer/helpers/array';
import { StackStatus } from '@/react/common/stacks/types';
import {
@ -67,7 +67,7 @@ function NameCell({
}
function NameLink({ item }: { item: DecoratedStack }) {
const { isAdmin } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
const name = item.Name;
@ -87,7 +87,7 @@ function NameLink({ item }: { item: DecoratedStack }) {
);
}
if (!isAdmin && isOrphanedStack(item)) {
if (!isAdminQuery.isAdmin && isOrphanedStack(item)) {
return <>{name}</>;
}

View File

@ -4,6 +4,7 @@ import { notifySuccess } from '@/portainer/services/notifications';
import { useDeleteEnvironmentsMutation } from '@/react/portainer/environments/queries/useDeleteEnvironmentsMutation';
import { Environment } from '@/react/portainer/environments/types';
import { withReactQuery } from '@/react-tools/withReactQuery';
import { useIsPureAdmin } from '@/react/hooks/useUser';
import { Button } from '@@/buttons';
import { ModalType, openModal } from '@@/modals';
@ -28,6 +29,7 @@ export function TableActions({
}: {
selectedRows: WaitingRoomEnvironment[];
}) {
const isPureAdmin = useIsPureAdmin();
const associateMutation = useAssociateDeviceMutation();
const removeMutation = useDeleteEnvironmentsMutation();
const licenseOverused = useLicenseOverused(selectedRows.length);
@ -58,7 +60,9 @@ export function TableActions({
<span>
<Button
onClick={() => handleAssociateAndAssign(selectedRows)}
disabled={selectedRows.length === 0 || licenseOverused}
disabled={
selectedRows.length === 0 || licenseOverused || !isPureAdmin
}
color="secondary"
icon={CheckCircle}
>

View File

@ -1,13 +1,10 @@
import { useRouter } from '@uirouter/react';
import { useEffect } from 'react';
import { EnvironmentId } from '../portainer/environments/types';
import { useAuthorizations } from './useUser';
type AuthorizationOptions = {
authorizations: string | string[];
forceEnvironmentId?: EnvironmentId;
adminOnlyCE?: boolean;
};
@ -19,24 +16,19 @@ type RedirectOptions = {
/**
* Redirects to the given route if the user is not authorized.
* @param authorizations The authorizations to check.
* @param forceEnvironmentId The environment id to use for the check.
* @param adminOnlyCE Whether to check only for admin authorizations in CE.
* @param adminOnlyCE Whether to allow non-admin users in CE.
* @param to The route to redirect to.
* @param params The params to pass to the route.
*/
export function useUnauthorizedRedirect(
{
authorizations,
forceEnvironmentId,
adminOnlyCE = false,
}: AuthorizationOptions,
{ authorizations, adminOnlyCE = false }: AuthorizationOptions,
{ to, params }: RedirectOptions
) {
const router = useRouter();
const isAuthorized = useAuthorizations(
authorizations,
forceEnvironmentId,
undefined,
adminOnlyCE
);

View File

@ -7,11 +7,14 @@ import {
PropsWithChildren,
} from 'react';
import { isAdmin } from '@/portainer/users/user.helpers';
import { isEdgeAdmin, isPureAdmin } from '@/portainer/users/user.helpers';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { User } from '@/portainer/users/types';
import { useLoadCurrentUser } from '@/portainer/users/queries/useLoadCurrentUser';
import { useEnvironment } from '../portainer/environments/queries';
import { isBE } from '../portainer/feature-flags/feature-flags.service';
interface State {
user?: User;
}
@ -39,32 +42,84 @@ export function useCurrentUser() {
return useMemo(
() => ({
user,
isAdmin: isAdmin(user),
isPureAdmin: isPureAdmin(user),
}),
[user]
);
}
export function useIsPureAdmin() {
const { isPureAdmin } = useCurrentUser();
return isPureAdmin;
}
/**
* Load the admin status of the user, (admin >= edge admin)
* @param forceEnvironmentId to force the environment id, used where the environment id can't be loaded from the router, like sidebar
* @returns query result with isLoading and isAdmin - isAdmin is true if the user edge admin or admin.
*/
export function useIsEdgeAdmin({
forceEnvironmentId,
noEnvScope,
}: {
forceEnvironmentId?: EnvironmentId;
noEnvScope?: boolean;
} = {}) {
const { user } = useCurrentUser();
const {
params: { endpointId },
} = useCurrentStateAndParams();
const envId = forceEnvironmentId || endpointId;
const envScope = typeof noEnvScope === 'boolean' ? !noEnvScope : !!envId;
const envQuery = useEnvironment(envScope ? envId : undefined);
if (!envScope) {
return { isLoading: false, isAdmin: isEdgeAdmin(user) };
}
if (envQuery.isLoading) {
return { isLoading: true, isAdmin: false };
}
return {
isLoading: false,
isAdmin: isEdgeAdmin(user, envQuery.data),
};
}
export function useAuthorizations(
authorizations: string | string[],
forceEnvironmentId?: EnvironmentId,
adminOnlyCE = false
) {
const { user } = useUser();
const { user } = useCurrentUser();
const {
params: { endpointId },
} = useCurrentStateAndParams();
const envQuery = useEnvironment(forceEnvironmentId || endpointId);
const isAdmin = useIsEdgeAdmin({ forceEnvironmentId });
if (!user) {
return false;
return { authorized: false, isLoading: false };
}
return hasAuthorizations(
user,
authorizations,
forceEnvironmentId || endpointId,
adminOnlyCE
);
if (envQuery.isLoading) {
return { authorized: false, isLoading: true };
}
if (isAdmin) {
return { authorized: true, isLoading: false };
}
if (!isBE && adminOnlyCE) {
return { authorized: false, isLoading: false };
}
return {
authorized: hasAuthorizations(user, authorizations, envQuery.data?.Id),
isLoading: false,
};
}
export function useIsEnvironmentAdmin({
@ -81,24 +136,18 @@ export function useIsEnvironmentAdmin({
);
}
export function isEnvironmentAdmin(
user: User,
environmentId: EnvironmentId,
adminOnlyCE = true
) {
return hasAuthorizations(
user,
['EndpointResourcesAccess'],
environmentId,
adminOnlyCE
);
}
export function hasAuthorizations(
/**
* will return true if the user has the authorizations. assumes the user is authenticated and not an admin
* @param user
* @param authorizations
* @param environmentId
* @param adminOnlyCE
* @returns
*/
function hasAuthorizations(
user: User,
authorizations: string | string[],
environmentId?: EnvironmentId,
adminOnlyCE = false
environmentId?: EnvironmentId
) {
const authorizationsArray =
typeof authorizations === 'string' ? [authorizations] : authorizations;
@ -107,26 +156,13 @@ export function hasAuthorizations(
return true;
}
if (process.env.PORTAINER_EDITION === 'CE') {
return !adminOnlyCE || isAdmin(user);
}
if (!environmentId) {
return false;
}
if (isAdmin(user)) {
return true;
}
const userEndpointAuthorizations =
user.EndpointAuthorizations?.[environmentId] || [];
if (
!user.EndpointAuthorizations ||
!user.EndpointAuthorizations[environmentId]
) {
return false;
}
const userEndpointAuthorizations = user.EndpointAuthorizations[environmentId];
return authorizationsArray.some(
(authorization) => userEndpointAuthorizations[authorization]
);

View File

@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { useCurrentUser } from '@/react/hooks/useUser';
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
import { Link } from '@@/Link';
import { TextTip } from '@@/Tip/TextTip';
@ -22,11 +22,14 @@ export function StackName({
inputClassName,
textTip = "Enter or select a 'stack' name to group multiple deployments together, or else leave empty to ignore.",
}: Props) {
const { isAdmin } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
const stackResults = useMemo(
() => stacks.filter((stack) => stack.includes(stackName ?? '')),
[stacks, stackName]
);
const { isAdmin } = isAdminQuery;
const tooltip = (
<>
You may specify a stack name to label resources that you want to group.

View File

@ -1,7 +1,7 @@
import { Plus, RefreshCw } from 'lucide-react';
import { FormikErrors } from 'formik';
import { useCurrentUser } from '@/react/hooks/useUser';
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
import { useEnvironment } from '@/react/portainer/environments/queries';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
@ -39,7 +39,8 @@ export function LoadBalancerServicesForm({
namespace,
isEditMode,
}: Props) {
const { isAdmin } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
const environmentId = useEnvironmentId();
const { data: loadBalancerEnabled, ...loadBalancerEnabledQuery } =
useEnvironment(
@ -47,6 +48,12 @@ export function LoadBalancerServicesForm({
(environment) => environment?.Kubernetes.Configuration.UseLoadBalancer
);
if (isAdminQuery.isLoading) {
return null;
}
const { isAdmin } = isAdminQuery;
const loadBalancerServiceCount = services.filter(
(service) => service.Type === 'LoadBalancer'
).length;

View File

@ -1,6 +1,6 @@
import { FormikErrors } from 'formik';
import { useCurrentUser } from '@/react/hooks/useUser';
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
import { SwitchField } from '@@/form-components/SwitchField';
import { Link } from '@@/Link';
@ -113,7 +113,13 @@ export function AutoScalingFormSection({
}
function NoMetricsServerWarning() {
const { isAdmin } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
if (isAdminQuery.isLoading) {
return null;
}
const { isAdmin } = isAdminQuery;
return (
<TextTip color="orange">
{isAdmin && (

View File

@ -12,7 +12,6 @@ export function ConfigureView() {
useUnauthorizedRedirect(
{
authorizations: 'K8sClusterW',
forceEnvironmentId: environment?.Id,
adminOnlyCE: false,
},
{

View File

@ -12,7 +12,6 @@ export function CreateNamespaceView() {
useUnauthorizedRedirect(
{
authorizations: 'K8sResourcePoolsW',
forceEnvironmentId: environmentId,
adminOnlyCE: !isBE,
},
{

View File

@ -19,13 +19,13 @@ export function RegistriesSelector({
options = [],
inputId,
}: Props) {
const { isAdmin } = useCurrentUser();
const { isPureAdmin } = useCurrentUser();
return (
<>
{options.length === 0 && (
<p className="text-muted text-xs mb-1 mt-2">
{isAdmin ? (
{isPureAdmin ? (
<span>
No registries available. Head over to the{' '}
<Link to="portainer.registries" target="_blank">

View File

@ -2,7 +2,7 @@ import { Edit2, Settings } from 'lucide-react';
import { ReactNode } from 'react';
import clsx from 'clsx';
import { useUser } from '@/react/hooks/useUser';
import { useCurrentUser } from '@/react/hooks/useUser';
import {
Environment,
PlatformType,
@ -15,7 +15,7 @@ import {
import { LinkButton } from '@@/LinkButton';
export function EditButtons({ environment }: { environment: Environment }) {
const { isAdmin } = useUser();
const { isPureAdmin } = useCurrentUser();
const isEdgeAsync = checkEdgeAsync(environment);
@ -31,7 +31,7 @@ export function EditButtons({ environment }: { environment: Environment }) {
return (
<ButtonsGrid className="ml-3 w-11">
<LinkButton
disabled={!isAdmin}
disabled={!isPureAdmin}
to="portainer.endpoints.endpoint"
params={{ id: environment.Id, redirectTo: 'portainer.home' }}
color="none"
@ -42,7 +42,7 @@ export function EditButtons({ environment }: { environment: Environment }) {
/>
<LinkButton
disabled={!configRoute || isEdgeAsync || !isAdmin}
disabled={!configRoute || isEdgeAsync || !isPureAdmin}
to={configRoute}
params={{ endpointId: environment.Id }}
color="none"

View File

@ -18,7 +18,7 @@ import {
} from '@/react/portainer/environments/queries/useEnvironmentList';
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service';
import { useUser } from '@/react/hooks/useUser';
import { useIsPureAdmin } from '@/react/hooks/useUser';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { environmentStore } from '@/react/hooks/current-environment-store';
@ -46,7 +46,7 @@ interface Props {
const storageKey = 'home_endpoints';
export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
const { isAdmin } = useUser();
const isPureAdmin = useIsPureAdmin();
const currentEnvStore = useStore(environmentStore);
const [platformTypes, setPlatformTypes] = useHomePageFilter<PlatformType[]>(
@ -138,7 +138,9 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
return (
<>
{totalAvailable === 0 && <NoEnvironmentsInfoPanel isAdmin={isAdmin} />}
{totalAvailable === 0 && (
<NoEnvironmentsInfoPanel isAdmin={isPureAdmin} />
)}
<TableContainer>
<div className="px-4">
@ -160,7 +162,7 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
placeholder="Search by name, group, tag, status, URL..."
data-cy="home-endpointsSearchInput"
/>
{isAdmin && (
{isPureAdmin && (
<Button
onClick={onRefresh}
data-cy="home-refreshEndpointsButton"

View File

@ -1,6 +1,6 @@
import { FormikErrors } from 'formik';
import { useUser } from '@/react/hooks/useUser';
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
import { SwitchField } from '@@/form-components/SwitchField';
@ -26,7 +26,13 @@ export function AccessControlForm({
errors,
environmentId,
}: Props) {
const { isAdmin } = useUser();
const isAdminQuery = useIsEdgeAdmin();
if (isAdminQuery.isLoading) {
return null;
}
const { isAdmin } = isAdminQuery;
const accessControlEnabled =
values.ownership !== ResourceControlOwnership.PUBLIC;

View File

@ -1,11 +1,14 @@
import { useReducer } from 'react';
import { Edit, Eye } from 'lucide-react';
import { useUser } from '@/react/hooks/useUser';
import { Icon } from '@/react/components/Icon';
import { TeamMembership, TeamRole } from '@/react/portainer/users/teams/types';
import { useIsTeamLeader, useUserMembership } from '@/portainer/users/queries';
import {
useIsCurrentUserTeamLeader,
useUserMembership,
} from '@/portainer/users/queries';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { TableContainer, TableTitle } from '@@/datatables';
import { Button } from '@@/buttons';
@ -34,20 +37,27 @@ export function AccessControlPanel({
onUpdateSuccess,
}: Props) {
const [isEditMode, toggleEditMode] = useReducer((state) => !state, false);
const { user, isAdmin } = useUser();
const isAdminQuery = useIsEdgeAdmin();
const isTeamLeader = useIsCurrentUserTeamLeader();
const isInherited = checkIfInherited();
const restrictions = useRestrictions(resourceControl);
if (isAdminQuery.isLoading || !restrictions) {
return null;
}
const { isPartOfRestrictedUsers, isLeaderOfAnyRestrictedTeams } =
useRestrictions(resourceControl);
restrictions;
const { isAdmin } = isAdminQuery;
const isEditDisabled =
disableOwnershipChange ||
isInherited ||
(!isAdmin && !isPartOfRestrictedUsers && !isLeaderOfAnyRestrictedTeams);
const isTeamLeader = useIsTeamLeader(user) as boolean;
return (
<TableContainer>
<TableTitle label="Access control" icon={Eye} />
@ -106,10 +116,16 @@ export function AccessControlPanel({
}
function useRestrictions(resourceControl?: ResourceControlViewModel) {
const { user, isAdmin } = useUser();
const { user } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
const memberships = useUserMembership(user.Id);
if (isAdminQuery.isLoading) {
return undefined;
}
const { isAdmin } = isAdminQuery;
if (!resourceControl || isAdmin) {
return {
isPartOfRestrictedUsers: false,

View File

@ -3,7 +3,7 @@ import clsx from 'clsx';
import { useMutation } from 'react-query';
import { object } from 'yup';
import { useCurrentUser } from '@/react/hooks/useUser';
import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { notifySuccess } from '@/portainer/services/notifications';
import { EnvironmentId } from '@/react/portainer/environments/types';
@ -43,7 +43,8 @@ export function AccessControlPanelForm({
onCancelClick,
onUpdateSuccess,
}: Props) {
const { isAdmin, user } = useCurrentUser();
const { user } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
const updateAccess = useMutation(
(variables: AccessControlFormData) =>
@ -63,6 +64,12 @@ export function AccessControlPanelForm({
}
);
if (isAdminQuery.isLoading) {
return null;
}
const { isAdmin } = isAdminQuery;
const initialValues = {
accessControl: parseAccessControlFormData(
isAdmin,

View File

@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { FormikErrors } from 'formik';
import { useUser } from '@/react/hooks/useUser';
import { useCurrentUser } from '@/react/hooks/useUser';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { FormError } from '@@/form-components/FormError';
@ -30,9 +30,10 @@ export function EditDetails({
formNamespace,
environmentId,
}: Props) {
const { user, isAdmin } = useUser();
const { user, isPureAdmin } = useCurrentUser();
const { users, teams, isLoading } = useLoadState(environmentId);
const { users, teams, isLoading } = useLoadState(environmentId, isAdmin);
const handleChange = useCallback(
(partialValues: Partial<typeof values>) => {
onChange({ ...values, ...partialValues });
@ -41,7 +42,12 @@ export function EditDetails({
[values, onChange]
);
if (isLoading || !teams || (isAdmin && !users) || !values.authorizedUsers) {
if (
isLoading ||
!teams ||
(isPureAdmin && !users) ||
!values.authorizedUsers
) {
return null;
}
@ -51,14 +57,14 @@ export function EditDetails({
onChange={handleChangeOwnership}
name={withNamespace('ownership')}
value={values.ownership}
isAdmin={isAdmin}
isAdmin={isPureAdmin}
isPublicVisible={isPublicVisible}
teams={teams}
/>
{values.ownership === ResourceControlOwnership.RESTRICTED && (
<div aria-label="extra-options">
{isAdmin && (
{isPureAdmin && (
<UsersField
name={withNamespace('authorizedUsers')}
users={users || []}
@ -68,12 +74,12 @@ export function EditDetails({
/>
)}
{(isAdmin || teams.length > 1) && (
{(isPureAdmin || teams.length > 1) && (
<TeamsField
name={withNamespace('authorizedTeams')}
teams={teams}
overrideTooltip={
!isAdmin && teams.length > 1
!isPureAdmin && teams.length > 1
? 'As you are a member of multiple teams, you can select which teams(s) will be able to manage this resource.'
: undefined
}
@ -111,7 +117,7 @@ export function EditDetails({
// Non admin team leaders/members under only one team can
// automatically grant the resource access to all members
// under the team
if (!isAdmin && teams && teams.length === 1) {
if (!isPureAdmin && teams && teams.length === 1) {
authorizedTeams = teams.map((team) => team.Id);
}
}

View File

@ -1,15 +1,18 @@
import { useTeams } from '@/react/portainer/users/teams/queries';
import { useUsers } from '@/portainer/users/queries';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
export function useLoadState(environmentId: EnvironmentId, enabled = true) {
export function useLoadState(environmentId: EnvironmentId) {
const isAdminQuery = useIsEdgeAdmin();
const teams = useTeams(false, environmentId);
const users = useUsers(false, environmentId, enabled);
const users = useUsers(false, environmentId, isAdminQuery.isAdmin);
return {
teams: teams.data,
users: users.data,
isLoading: teams.isLoading || users.isLoading,
isAdmin: isAdminQuery.isAdmin,
isLoading: teams.isLoading || users.isLoading || isAdminQuery.isLoading,
};
}

View File

@ -7,14 +7,14 @@ import { Environment, EnvironmentId } from '../types';
import { environmentQueryKeys } from './query-keys';
export function useEnvironment<T = Environment | null>(
export function useEnvironment<T = Environment>(
environmentId?: EnvironmentId,
select?: (environment: Environment | null) => T,
select?: (environment: Environment) => T,
options?: { autoRefreshRate?: number }
) {
return useQuery(
environmentId ? environmentQueryKeys.item(environmentId) : [],
() => (environmentId ? getEndpoint(environmentId) : null),
environmentQueryKeys.item(environmentId!),
() => getEndpoint(environmentId!),
{
select,
...withError('Failed loading environment'),

View File

@ -1,24 +1,27 @@
import { useField } from 'formik';
import { PropsWithChildren } from 'react';
import { useUser } from '@/react/hooks/useUser';
import { useCurrentUser } from '@/react/hooks/useUser';
import { TagSelector } from '@@/TagSelector';
import { FormSection } from '@@/form-components/FormSection';
import { GroupField } from './GroupsField';
export function MetadataFieldset() {
export function MetadataFieldset({ children }: PropsWithChildren<unknown>) {
const [tagProps, , tagHelpers] = useField('meta.tagIds');
const { isAdmin } = useUser();
const { isPureAdmin } = useCurrentUser();
return (
<FormSection title="Metadata">
{children}
<GroupField />
<TagSelector
value={tagProps.value}
allowCreate={isAdmin}
allowCreate={isPureAdmin}
onChange={(value) => tagHelpers.setValue(value)}
/>
</FormSection>

View File

@ -1,7 +1,7 @@
import { useCurrentStateAndParams } from '@uirouter/react';
import { parseAccessControlFormData } from '@/react/portainer/access-control/utils';
import { useCurrentUser } from '@/react/hooks/useUser';
import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { StackType } from '@/react/common/stacks/types';
import { Platform } from '../../types';
@ -19,11 +19,13 @@ export function useInitialValues({
isEdge?: boolean;
buildMethods: Array<Method>;
}): FormValues | undefined {
const { user, isAdmin } = useCurrentUser();
const { user } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
const { appTemplateId, type = defaultType } = useAppTemplateParams();
const fileContentQuery = useFetchTemplateFile(appTemplateId);
if (fileContentQuery.isLoading) {
if (fileContentQuery.isLoading || isAdminQuery.isLoading) {
return undefined;
}
@ -51,7 +53,7 @@ export function useInitialValues({
},
AccessControl: isEdge
? undefined
: parseAccessControlFormData(isAdmin, user.Id),
: parseAccessControlFormData(isAdminQuery.isAdmin, user.Id),
EdgeSettings: isEdge ? getDefaultEdgeTemplateSettings() : undefined,
};
}

View File

@ -46,7 +46,7 @@ export function EditForm({
templateFile: fileContentQuery.data,
});
if (fileContentQuery.isLoading) {
if (fileContentQuery.isLoading || !initialValues) {
return null;
}

View File

@ -1,5 +1,5 @@
import { parseAccessControlFormData } from '@/react/portainer/access-control/utils';
import { useCurrentUser } from '@/react/hooks/useUser';
import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { toGitFormModel } from '@/react/portainer/gitops/types';
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
@ -15,8 +15,14 @@ export function useInitialValues({
template: CustomTemplate;
templateFile: string | undefined;
isEdge: boolean;
}): FormValues {
const { user, isAdmin } = useCurrentUser();
}): FormValues | undefined {
const { user } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
if (isAdminQuery.isLoading) {
return undefined;
}
return {
Title: template.Title,
@ -31,7 +37,7 @@ export function useInitialValues({
AccessControl:
!isEdge && template.ResourceControl
? parseAccessControlFormData(
isAdmin,
isAdminQuery.isAdmin,
user.Id,
new ResourceControlViewModel(template.ResourceControl)
)

View File

@ -1,6 +1,6 @@
import { Edit, Trash2 } from 'lucide-react';
import { useCurrentUser } from '@/react/hooks/useUser';
import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
import { StackType } from '@/react/common/stacks/types';
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
@ -22,8 +22,15 @@ export function CustomTemplatesListItem({
isSelected: boolean;
linkParams?: { to: string; params: object };
}) {
const { isAdmin, user } = useCurrentUser();
const isEditAllowed = isAdmin || template.CreatedByUserId === user.Id;
const { user } = useCurrentUser();
const isAdminQuery = useIsEdgeAdmin();
if (isAdminQuery.isLoading) {
return null;
}
const isEditAllowed =
isAdminQuery.isAdmin || template.CreatedByUserId === user.Id;
return (
<TemplateItem

View File

@ -1,7 +1,7 @@
import { useRouter } from '@uirouter/react';
import { useUsers } from '@/portainer/users/queries';
import { useUser } from '@/react/hooks/useUser';
import { useIsPureAdmin } from '@/react/hooks/useUser';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import { TextTip } from '@@/Tip/TextTip';
@ -16,7 +16,7 @@ import { useTeamIdParam } from './useTeamIdParam';
export function ItemView() {
const teamId = useTeamIdParam();
const { isAdmin } = useUser();
const isPureAdmin = useIsPureAdmin();
const router = useRouter();
const teamQuery = useTeam(teamId, () =>
router.stateService.go('portainer.teams')
@ -45,7 +45,7 @@ export function ItemView() {
<Details
team={team}
memberships={membershipsQuery.data}
isAdmin={isAdmin}
isAdmin={isPureAdmin}
/>
)}

View File

@ -3,7 +3,7 @@ import { Users, UserX } from 'lucide-react';
import { User, UserId } from '@/portainer/users/types';
import { TeamId, TeamRole } from '@/react/portainer/users/teams/types';
import { useUser } from '@/react/hooks/useUser';
import { useIsPureAdmin } from '@/react/hooks/useUser';
import { notifySuccess } from '@/portainer/services/notifications';
import {
useRemoveMemberMutation,
@ -37,8 +37,7 @@ export function TeamMembersList({ users, roles, disabled, teamId }: Props) {
{ id: string; desc: boolean } | undefined
>({ id: 'name', desc: false });
const { isAdmin } = useUser();
const isPureAdmin = useIsPureAdmin();
const rowContext = useMemo<RowContext>(
() => ({
getRole(userId: UserId) {
@ -58,7 +57,7 @@ export function TeamMembersList({ users, roles, disabled, teamId }: Props) {
titleIcon={Users}
title="Team members"
renderTableActions={() =>
isAdmin && (
isPureAdmin && (
<Button
onClick={() => handleRemoveMembers(users.map((user) => user.Id))}
disabled={disabled || users.length === 0}

View File

@ -2,7 +2,7 @@ import { User as UserIcon, UserPlus, UserX } from 'lucide-react';
import { CellContext } from '@tanstack/react-table';
import { User } from '@/portainer/users/types';
import { useUser as useCurrentUser } from '@/react/hooks/useUser';
import { useCurrentUser } from '@/react/hooks/useUser';
import { TeamRole } from '@/react/portainer/users/teams/types';
import { notifySuccess } from '@/portainer/services/notifications';
import {
@ -23,7 +23,7 @@ export const teamRole = columnHelper.accessor('Id', {
cell: RoleCell,
});
export function RoleCell({
function RoleCell({
row: { original: user },
getValue,
}: CellContext<User, User['Id']>) {
@ -38,12 +38,16 @@ export function RoleCell({
const role = getRole(id);
const { isAdmin } = useCurrentUser();
const { isPureAdmin } = useCurrentUser();
const Cell = role === TeamRole.Leader ? LeaderCell : MemberCell;
return (
<Cell isAdmin={isAdmin} onClick={handleUpdateRole} disabled={disabled} />
<Cell
isAdmin={isPureAdmin}
onClick={handleUpdateRole}
disabled={disabled}
/>
);
function handleUpdateRole(role: TeamRole, onSuccessMessage: string) {

View File

@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
import { UserPlus, Users } from 'lucide-react';
import { User, UserId } from '@/portainer/users/types';
import { useUser } from '@/react/hooks/useUser';
import { useCurrentUser } from '@/react/hooks/useUser';
import { notifySuccess } from '@/portainer/services/notifications';
import { useAddMemberMutation } from '@/react/portainer/users/teams/queries';
import { TeamId } from '@/react/portainer/users/teams/types';
@ -29,7 +29,7 @@ export function UsersList({ users, disabled, teamId }: Props) {
{ id: string; desc: boolean } | undefined
>({ id: 'name', desc: false });
const { isAdmin } = useUser();
const { isPureAdmin } = useCurrentUser();
const rowContext = useMemo(() => ({ disabled, teamId }), [disabled, teamId]);
@ -41,7 +41,7 @@ export function UsersList({ users, disabled, teamId }: Props) {
titleIcon={Users}
title="Users"
renderTableActions={() =>
isAdmin && (
isPureAdmin && (
<Button
onClick={() => handleAddAllMembers(users.map((u) => u.Id))}
disabled={disabled || users.length === 0}

View File

@ -1,5 +1,5 @@
import { useUsers } from '@/portainer/users/queries';
import { useUser } from '@/react/hooks/useUser';
import { useCurrentUser } from '@/react/hooks/useUser';
import { PageHeader } from '@@/PageHeader';
@ -9,10 +9,10 @@ import { CreateTeamForm } from './CreateTeamForm';
import { TeamsDatatable } from './TeamsDatatable';
export function ListView() {
const { isAdmin } = useUser();
const { isPureAdmin } = useCurrentUser();
const usersQuery = useUsers(false);
const teamsQuery = useTeams(!isAdmin, 0, { enabled: !!usersQuery.data });
const teamsQuery = useTeams(!isPureAdmin, 0);
return (
<>
@ -22,12 +22,12 @@ export function ListView() {
reload
/>
{isAdmin && usersQuery.data && teamsQuery.data && (
{isPureAdmin && usersQuery.data && teamsQuery.data && (
<CreateTeamForm users={usersQuery.data} teams={teamsQuery.data} />
)}
{teamsQuery.data && (
<TeamsDatatable teams={teamsQuery.data} isAdmin={isAdmin} />
<TeamsDatatable teams={teamsQuery.data} isAdmin={isPureAdmin} />
)}
</>
);

View File

@ -15,7 +15,7 @@ import {
type Environment,
type EnvironmentId,
} from '@/react/portainer/environments/types';
import { Authorized, useUser, isEnvironmentAdmin } from '@/react/hooks/useUser';
import { Authorized, useIsEnvironmentAdmin } from '@/react/hooks/useUser';
import { useInfo } from '@/react/docker/proxy/queries/useInfo';
import { useApiVersion } from '@/react/docker/proxy/queries/useVersion';
@ -30,11 +30,11 @@ interface Props {
}
export function DockerSidebar({ environmentId, environment }: Props) {
const { user } = useUser();
const isAdmin = isEnvironmentAdmin(user, environmentId);
const isEnvironmentAdmin = useIsEnvironmentAdmin({ adminOnlyCE: true });
const areStacksVisible =
isAdmin || environment.SecuritySettings.allowStackManagementForRegularUsers;
isEnvironmentAdmin ||
environment.SecuritySettings.allowStackManagementForRegularUsers;
const envInfoQuery = useInfo(
environmentId,
@ -167,7 +167,7 @@ export function DockerSidebar({ environmentId, environment }: Props) {
/>
)}
{!isSwarmManager && isAdmin && (
{!isSwarmManager && isEnvironmentAdmin && (
<SidebarItem
to="docker.events"
params={{ endpointId: environmentId }}

View File

@ -12,7 +12,7 @@ import clsx from 'clsx';
import { useSystemStatus } from '@/react/portainer/system/useSystemStatus';
import { useSystemVersion } from '@/react/portainer/system/useSystemVersion';
import { useCurrentUser } from '@/react/hooks/useUser';
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
import { Modal } from '@@/modals';
import { Button } from '@@/buttons';
@ -47,7 +47,7 @@ export function BuildInfoModalButton() {
}
function BuildInfoModal({ closeModal }: { closeModal: () => void }) {
const { isAdmin } = useCurrentUser();
const { isAdmin } = useIsEdgeAdmin({ noEnvScope: true });
const versionQuery = useSystemVersion();
const statusQuery = useSystemStatus();

View File

@ -16,17 +16,19 @@ import { SidebarSection } from './SidebarSection';
import { SidebarParent } from './SidebarItem/SidebarParent';
interface Props {
isPureAdmin: boolean;
isAdmin: boolean;
isTeamLeader?: boolean;
}
export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
export function SettingsSidebar({ isPureAdmin, isAdmin, isTeamLeader }: Props) {
const teamSyncQuery = usePublicSettings<boolean>({
select: (settings) => settings.TeamSync,
});
const showUsersSection =
!window.ddExtension && (isAdmin || (isTeamLeader && !teamSyncQuery.data));
const isPureAdminOrTeamLeader =
isPureAdmin || (isTeamLeader && !teamSyncQuery.data && !isAdmin);
const showUsersSection = !window.ddExtension && isPureAdminOrTeamLeader;
return (
<SidebarSection title="Administration">
@ -51,7 +53,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
data-cy="portainerSidebar-teams"
/>
{isAdmin && (
{isPureAdmin && (
<SidebarItem
to="portainer.roles"
label="Roles"
@ -61,7 +63,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
)}
</SidebarParent>
)}
{isAdmin && (
{isPureAdmin && (
<>
<SidebarParent
label="Environment-related"
@ -74,7 +76,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
'portainer.tags',
],
}}
data-cy="k8sSidebar-networking"
data-cy="portainerSidebar-environments-area"
>
<SidebarItem
label="Environments"
@ -139,13 +141,24 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
</SidebarParent>
</>
)}
{isBE && !isPureAdmin && isAdmin && (
<SidebarParent
label="Environment-related"
icon={HardDrive}
to="portainer.endpoints.updateSchedules"
data-cy="portainerSidebar-environments-area"
>
<EdgeUpdatesSidebarItem />
</SidebarParent>
)}
<SidebarItem
to="portainer.notifications"
icon={Bell}
label="Notifications"
data-cy="portainerSidebar-notifications"
/>
{isAdmin && (
{isPureAdmin && (
<SidebarParent
to="portainer.settings"
label="Settings"
@ -158,6 +171,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
isSubMenu
ignorePaths={[
'portainer.settings.authentication',
'portainer.settings.sharedcredentials',
'portainer.settings.edgeCompute',
]}
data-cy="portainerSidebar-generalSettings"
@ -178,6 +192,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
data-cy="portainerSidebar-cloud"
/>
)}
<SidebarItem
to="portainer.settings.edgeCompute"
label="Edge Compute"

View File

@ -1,8 +1,8 @@
import clsx from 'clsx';
import { Home } from 'lucide-react';
import { useUser } from '@/react/hooks/useUser';
import { useIsTeamLeader } from '@/portainer/users/queries';
import { useIsEdgeAdmin, useIsPureAdmin } from '@/react/hooks/useUser';
import { useIsCurrentUserTeamLeader } from '@/portainer/users/queries';
import { usePublicSettings } from '@/react/portainer/settings/queries';
import styles from './Sidebar.module.css';
@ -25,16 +25,19 @@ export function Sidebar() {
}
function InnerSidebar() {
const { isAdmin, user } = useUser();
const isTeamLeader = useIsTeamLeader(user) as boolean;
const isPureAdmin = useIsPureAdmin();
const isAdminQuery = useIsEdgeAdmin({ noEnvScope: true });
const isTeamLeader = useIsCurrentUserTeamLeader();
const { isOpen } = useSidebarState();
const settingsQuery = usePublicSettings();
if (!settingsQuery.data) {
if (!settingsQuery.data || isAdminQuery.isLoading) {
return null;
}
const { isAdmin } = isAdminQuery;
const { LogoURL } = settingsQuery.data;
return (
@ -66,7 +69,11 @@ function InnerSidebar() {
/>
<EnvironmentSidebar />
{isAdmin && <EdgeComputeSidebar />}
<SettingsSidebar isAdmin={isAdmin} isTeamLeader={isTeamLeader} />
<SettingsSidebar
isPureAdmin={isPureAdmin}
isAdmin={isAdmin}
isTeamLeader={isTeamLeader}
/>
</ul>
</div>
<div className="mt-auto pt-8">

View File

@ -29,7 +29,7 @@ const enabledPlatforms: Array<ContainerPlatform> = [
function UpgradeBEBanner() {
const {
isAdmin,
isPureAdmin,
user: { Id },
} = useCurrentUser();
@ -90,7 +90,7 @@ function UpgradeBEBanner() {
function handleClick() {
trackEvent(
isAdmin ? 'portainer-upgrade-admin' : 'portainer-upgrade-non-admin',
isPureAdmin ? 'portainer-upgrade-admin' : 'portainer-upgrade-non-admin',
{
category: 'portainer',
metadata,

View File

@ -1,6 +1,6 @@
import { useState } from 'react';
import { useUser } from '@/react/hooks/useUser';
import { useCurrentUser } from '@/react/hooks/useUser';
import { UploadLicenseDialog } from './UploadLicenseDialog';
import { LoadingDialog } from './LoadingDialog';
@ -10,7 +10,7 @@ import { GetLicenseDialog } from './GetLicenseDialog';
type Step = 'uploadLicense' | 'loading' | 'getLicense';
export function UpgradeDialog({ onDismiss }: { onDismiss: () => void }) {
const { isAdmin } = useUser();
const { isPureAdmin } = useCurrentUser();
const [currentStep, setCurrentStep] = useState<Step>('uploadLicense');
const [isGetLicenseSubmitted, setIsGetLicenseSubmitted] = useState(false);
const component = getDialog();
@ -18,7 +18,7 @@ export function UpgradeDialog({ onDismiss }: { onDismiss: () => void }) {
return component;
function getDialog() {
if (!isAdmin) {
if (!isPureAdmin) {
return <NonAdminUpgradeDialog onDismiss={onDismiss} />;
}