mirror of https://github.com/portainer/portainer
feat(auth): add useIsEdgeAdmin hook [EE-6627] (#11101)
parent
c08b5af85a
commit
edea9e3481
|
@ -64,7 +64,7 @@ angular.module('portainer.app').controller('porAccessControlFormController', [
|
||||||
|
|
||||||
this.$onInit = $onInit;
|
this.$onInit = $onInit;
|
||||||
function $onInit() {
|
function $onInit() {
|
||||||
var isAdmin = Authentication.isAdmin();
|
var isAdmin = Authentication.isPureAdmin();
|
||||||
ctrl.isAdmin = isAdmin;
|
ctrl.isAdmin = isAdmin;
|
||||||
|
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { getCurrentUser } from '../users/queries/useLoadCurrentUser';
|
import { getCurrentUser } from '../users/queries/useLoadCurrentUser';
|
||||||
|
import * as userHelpers from '../users/user.helpers';
|
||||||
import { clear as clearSessionStorage } from './session-storage';
|
import { clear as clearSessionStorage } from './session-storage';
|
||||||
|
|
||||||
const DEFAULT_USER = 'admin';
|
const DEFAULT_USER = 'admin';
|
||||||
|
@ -25,6 +26,9 @@ angular.module('portainer.app').factory('Authentication', [
|
||||||
service.isAuthenticated = isAuthenticated;
|
service.isAuthenticated = isAuthenticated;
|
||||||
service.getUserDetails = getUserDetails;
|
service.getUserDetails = getUserDetails;
|
||||||
service.isAdmin = isAdmin;
|
service.isAdmin = isAdmin;
|
||||||
|
service.isEdgeAdmin = isEdgeAdmin;
|
||||||
|
service.isPureAdmin = isPureAdmin;
|
||||||
|
service.hasAuthorizations = hasAuthorizations;
|
||||||
|
|
||||||
async function initAsync() {
|
async function initAsync() {
|
||||||
try {
|
try {
|
||||||
|
@ -120,8 +124,36 @@ angular.module('portainer.app').factory('Authentication', [
|
||||||
return login(DEFAULT_USER, DEFAULT_PASSWORD);
|
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() {
|
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') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
|
|
||||||
import { TeamRole, TeamMembership } from '@/react/portainer/users/teams/types';
|
import { TeamRole, TeamMembership } from '@/react/portainer/users/teams/types';
|
||||||
|
import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { User, UserId } from './types';
|
import { User, UserId } from './types';
|
||||||
import { isAdmin } from './user.helpers';
|
|
||||||
import { getUserMemberships, getUsers } from './user.service';
|
import { getUserMemberships, getUsers } from './user.service';
|
||||||
|
|
||||||
interface UseUserMembershipOptions<TSelect> {
|
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, {
|
const query = useUserMembership(user.Id, {
|
||||||
enabled: !isAdmin(user),
|
enabled: !isAdminQuery.isLoading && !isAdminQuery.isAdmin,
|
||||||
select: (memberships) =>
|
select: (memberships) =>
|
||||||
memberships.some((membership) => membership.Role === TeamRole.Leader),
|
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[]>(
|
export function useUsers<T = User[]>(
|
||||||
|
|
|
@ -7,6 +7,7 @@ export { type UserId };
|
||||||
export enum Role {
|
export enum Role {
|
||||||
Admin = 1,
|
Admin = 1,
|
||||||
Standard,
|
Standard,
|
||||||
|
EdgeAdmin,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthorizationMap {
|
interface AuthorizationMap {
|
||||||
|
|
|
@ -1,9 +1,30 @@
|
||||||
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
|
import { isEdgeEnvironment } from '@/react/portainer/environments/utils';
|
||||||
|
|
||||||
import { Role, User } from './types';
|
import { Role, User } from './types';
|
||||||
|
|
||||||
export function filterNonAdministratorUsers(users: User[]) {
|
export function filterNonAdministratorUsers(users: User[]) {
|
||||||
return users.filter((user) => user.Role !== Role.Admin);
|
return users.filter((user) => user.Role !== Role.Admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAdmin(user?: User): boolean {
|
type UserLike = Pick<User, 'Role'>;
|
||||||
return !!user && user.Role === 1;
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Plus } from 'lucide-react';
|
||||||
|
|
||||||
import { ContainerInstanceFormValues } from '@/react/azure/types';
|
import { ContainerInstanceFormValues } from '@/react/azure/types';
|
||||||
import * as notifications from '@/portainer/services/notifications';
|
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 { AccessControlForm } from '@/react/portainer/access-control/AccessControlForm';
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ import { useCreateInstanceMutation } from './useCreateInstanceMutation';
|
||||||
|
|
||||||
export function CreateContainerInstanceForm() {
|
export function CreateContainerInstanceForm() {
|
||||||
const environmentId = useEnvironmentId();
|
const environmentId = useEnvironmentId();
|
||||||
const { isAdmin } = useUser();
|
const { isPureAdmin } = useCurrentUser();
|
||||||
|
|
||||||
const { providers, subscriptions, resourceGroups, isLoading } =
|
const { providers, subscriptions, resourceGroups, isLoading } =
|
||||||
useLoadFormState(environmentId);
|
useLoadFormState(environmentId);
|
||||||
|
@ -49,7 +49,7 @@ export function CreateContainerInstanceForm() {
|
||||||
return (
|
return (
|
||||||
<Formik<ContainerInstanceFormValues>
|
<Formik<ContainerInstanceFormValues>
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
validationSchema={() => validationSchema(isAdmin)}
|
validationSchema={() => validationSchema(isPureAdmin)}
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
validateOnMount
|
validateOnMount
|
||||||
validateOnChange
|
validateOnChange
|
||||||
|
|
|
@ -37,7 +37,7 @@ export function useFormState(
|
||||||
resourceGroups: Record<string, ResourceGroup[]> = {},
|
resourceGroups: Record<string, ResourceGroup[]> = {},
|
||||||
providers: Record<string, ProviderViewModel> = {}
|
providers: Record<string, ProviderViewModel> = {}
|
||||||
) {
|
) {
|
||||||
const { isAdmin, user } = useCurrentUser();
|
const { user, isPureAdmin } = useCurrentUser();
|
||||||
|
|
||||||
const subscriptionOptions = subscriptions.map((s) => ({
|
const subscriptionOptions = subscriptions.map((s) => ({
|
||||||
value: s.subscriptionId,
|
value: s.subscriptionId,
|
||||||
|
@ -67,7 +67,7 @@ export function useFormState(
|
||||||
cpu: 1,
|
cpu: 1,
|
||||||
ports: [{ container: 80, host: 80, protocol: 'TCP' }],
|
ports: [{ container: 80, host: 80, protocol: 'TCP' }],
|
||||||
allocatePublicIP: true,
|
allocatePublicIP: true,
|
||||||
accessControl: parseAccessControlFormData(isAdmin, user.Id),
|
accessControl: parseAccessControlFormData(isPureAdmin, user.Id),
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -66,7 +66,7 @@ function RateLimitsInner({
|
||||||
environment: Environment;
|
environment: Environment;
|
||||||
}) {
|
}) {
|
||||||
const pullRateLimits = useRateLimits(registryId, environment, onRateLimit);
|
const pullRateLimits = useRateLimits(registryId, environment, onRateLimit);
|
||||||
const { isAdmin } = useCurrentUser();
|
const { isPureAdmin } = useCurrentUser();
|
||||||
|
|
||||||
if (!pullRateLimits) {
|
if (!pullRateLimits) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -88,7 +88,7 @@ function RateLimitsInner({
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{isAdmin ? (
|
{isPureAdmin ? (
|
||||||
<>
|
<>
|
||||||
You are currently using an anonymous account to pull images
|
You are currently using an anonymous account to pull images
|
||||||
from DockerHub and will be limited to 100 pulls every 6
|
from DockerHub and will be limited to 100 pulls every 6
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { Values } from './BaseForm';
|
||||||
|
|
||||||
export function toViewModel(
|
export function toViewModel(
|
||||||
config: ContainerResponse,
|
config: ContainerResponse,
|
||||||
isAdmin: boolean,
|
isPureAdmin: boolean,
|
||||||
currentUserId: UserId,
|
currentUserId: UserId,
|
||||||
nodeName: string,
|
nodeName: string,
|
||||||
image: Values['image'],
|
image: Values['image'],
|
||||||
|
@ -18,7 +18,7 @@ export function toViewModel(
|
||||||
): Values {
|
): Values {
|
||||||
// accessControl shouldn't be copied to new container
|
// accessControl shouldn't be copied to new container
|
||||||
|
|
||||||
const accessControl = parseAccessControlFormData(isAdmin, currentUserId);
|
const accessControl = parseAccessControlFormData(isPureAdmin, currentUserId);
|
||||||
|
|
||||||
if (config.Portainer?.ResourceControl?.Public) {
|
if (config.Portainer?.ResourceControl?.Public) {
|
||||||
accessControl.ownership = ResourceControlOwnership.PUBLIC;
|
accessControl.ownership = ResourceControlOwnership.PUBLIC;
|
||||||
|
@ -38,11 +38,11 @@ export function toViewModel(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultViewModel(
|
export function getDefaultViewModel(
|
||||||
isAdmin: boolean,
|
isPureAdmin: boolean,
|
||||||
currentUserId: UserId,
|
currentUserId: UserId,
|
||||||
nodeName: string
|
nodeName: string
|
||||||
): Values {
|
): Values {
|
||||||
const accessControl = parseAccessControlFormData(isAdmin, currentUserId);
|
const accessControl = parseAccessControlFormData(isPureAdmin, currentUserId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodeName,
|
nodeName,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Formik } from 'formik';
|
||||||
import { useRouter } from '@uirouter/react';
|
import { useRouter } from '@uirouter/react';
|
||||||
import { useEffect, useState } from '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 { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
import { useCurrentEnvironment } from '@/react/hooks/useCurrentEnvironment';
|
||||||
import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries';
|
import { useEnvironmentRegistries } from '@/react/portainer/environments/queries/useEnvironmentRegistries';
|
||||||
|
@ -48,7 +48,7 @@ function CreateForm() {
|
||||||
const environmentId = useEnvironmentId();
|
const environmentId = useEnvironmentId();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { trackEvent } = useAnalytics();
|
const { trackEvent } = useAnalytics();
|
||||||
const { isAdmin } = useCurrentUser();
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
const isEnvironmentAdmin = useIsEnvironmentAdmin();
|
const isEnvironmentAdmin = useIsEnvironmentAdmin();
|
||||||
const [isDockerhubRateLimited, setIsDockerhubRateLimited] = useState(false);
|
const [isDockerhubRateLimited, setIsDockerhubRateLimited] = useState(false);
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ function CreateForm() {
|
||||||
const envQuery = useCurrentEnvironment();
|
const envQuery = useCurrentEnvironment();
|
||||||
|
|
||||||
const validationSchema = useValidation({
|
const validationSchema = useValidation({
|
||||||
isAdmin,
|
isAdmin: isAdminQuery.isAdmin,
|
||||||
maxCpu,
|
maxCpu,
|
||||||
maxMemory,
|
maxMemory,
|
||||||
isDuplicating: initialValuesQuery?.isDuplicating,
|
isDuplicating: initialValuesQuery?.isDuplicating,
|
||||||
|
|
|
@ -102,7 +102,7 @@ export function InnerForm({
|
||||||
}
|
}
|
||||||
errors={errors.volumes}
|
errors={errors.volumes}
|
||||||
allowBindMounts={
|
allowBindMounts={
|
||||||
isEnvironmentAdmin ||
|
isEnvironmentAdmin.authorized ||
|
||||||
environment.SecuritySettings
|
environment.SecuritySettings
|
||||||
.allowBindMountsForRegularUsers
|
.allowBindMountsForRegularUsers
|
||||||
}
|
}
|
||||||
|
@ -166,18 +166,18 @@ export function InnerForm({
|
||||||
setFieldValue(`resources.${field}`, value)
|
setFieldValue(`resources.${field}`, value)
|
||||||
}
|
}
|
||||||
allowPrivilegedMode={
|
allowPrivilegedMode={
|
||||||
isEnvironmentAdmin ||
|
isEnvironmentAdmin.authorized ||
|
||||||
environment.SecuritySettings
|
environment.SecuritySettings
|
||||||
.allowPrivilegedModeForRegularUsers
|
.allowPrivilegedModeForRegularUsers
|
||||||
}
|
}
|
||||||
isDevicesFieldVisible={
|
isDevicesFieldVisible={
|
||||||
isEnvironmentAdmin ||
|
isEnvironmentAdmin.authorized ||
|
||||||
environment.SecuritySettings
|
environment.SecuritySettings
|
||||||
.allowDeviceMappingForRegularUsers
|
.allowDeviceMappingForRegularUsers
|
||||||
}
|
}
|
||||||
isInitFieldVisible={apiVersion >= 1.37}
|
isInitFieldVisible={apiVersion >= 1.37}
|
||||||
isSysctlFieldVisible={
|
isSysctlFieldVisible={
|
||||||
isEnvironmentAdmin ||
|
isEnvironmentAdmin.authorized ||
|
||||||
environment.SecuritySettings
|
environment.SecuritySettings
|
||||||
.allowSysctlSettingForRegularUsers
|
.allowSysctlSettingForRegularUsers
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,8 @@ export function useInitialValues(submitting: boolean) {
|
||||||
params: { nodeName, from },
|
params: { nodeName, from },
|
||||||
} = useCurrentStateAndParams();
|
} = useCurrentStateAndParams();
|
||||||
const environmentId = useEnvironmentId();
|
const environmentId = useEnvironmentId();
|
||||||
const { isAdmin, user } = useCurrentUser();
|
const { user, isPureAdmin } = useCurrentUser();
|
||||||
|
|
||||||
const networksQuery = useNetworksForSelector();
|
const networksQuery = useNetworksForSelector();
|
||||||
|
|
||||||
const fromContainerQuery = useContainer(environmentId, from, {
|
const fromContainerQuery = useContainer(environmentId, from, {
|
||||||
|
@ -85,7 +86,7 @@ export function useInitialValues(submitting: boolean) {
|
||||||
|
|
||||||
if (!from) {
|
if (!from) {
|
||||||
return {
|
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),
|
env: envVarsTabUtils.toViewModel(fromContainer),
|
||||||
...baseFormUtils.toViewModel(
|
...baseFormUtils.toViewModel(
|
||||||
fromContainer,
|
fromContainer,
|
||||||
isAdmin,
|
isPureAdmin,
|
||||||
user.Id,
|
user.Id,
|
||||||
nodeName,
|
nodeName,
|
||||||
imageConfig,
|
imageConfig,
|
||||||
|
@ -148,7 +149,7 @@ export function useInitialValues(submitting: boolean) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultValues(
|
function defaultValues(
|
||||||
isAdmin: boolean,
|
isPureAdmin: boolean,
|
||||||
currentUserId: UserId,
|
currentUserId: UserId,
|
||||||
nodeName: string
|
nodeName: string
|
||||||
): Values {
|
): Values {
|
||||||
|
@ -161,6 +162,6 @@ function defaultValues(
|
||||||
resources: resourcesTabUtils.getDefaultViewModel(),
|
resources: resourcesTabUtils.getDefaultViewModel(),
|
||||||
capabilities: capabilitiesTabUtils.getDefaultViewModel(),
|
capabilities: capabilitiesTabUtils.getDefaultViewModel(),
|
||||||
env: envVarsTabUtils.getDefaultViewModel(),
|
env: envVarsTabUtils.getDefaultViewModel(),
|
||||||
...baseFormUtils.getDefaultViewModel(isAdmin, currentUserId, nodeName),
|
...baseFormUtils.getDefaultViewModel(isPureAdmin, currentUserId, nodeName),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 { UserContext } from '@/react/hooks/useUser';
|
||||||
import { UserViewModel } from '@/portainer/models/user';
|
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) {
|
async function renderComponent(isAdmin: boolean, network: DockerNetwork) {
|
||||||
const user = new UserViewModel({ Username: 'test', Role: isAdmin ? 1 : 2 });
|
const user = new UserViewModel({ Username: 'test', Role: isAdmin ? 1 : 2 });
|
||||||
|
|
||||||
const queries = render(
|
const queries = renderWithQueryClient(
|
||||||
<UserContext.Provider value={{ user }}>
|
<UserContext.Provider value={{ user }}>
|
||||||
<NetworkDetailsTable
|
<NetworkDetailsTable
|
||||||
network={network}
|
network={network}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Layers } from 'lucide-react';
|
import { Layers } from 'lucide-react';
|
||||||
import { Row } from '@tanstack/react-table';
|
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 { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
import { Datatable } from '@@/datatables';
|
import { Datatable } from '@@/datatables';
|
||||||
|
@ -34,7 +34,7 @@ export function StacksDatatable({
|
||||||
}) {
|
}) {
|
||||||
const tableState = useTableState(settingsStore, tableKey);
|
const tableState = useTableState(settingsStore, tableKey);
|
||||||
useRepeater(tableState.autoRefreshRate, onReload);
|
useRepeater(tableState.autoRefreshRate, onReload);
|
||||||
const { isAdmin } = useCurrentUser();
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
const canManageStacks = useAuthorizations([
|
const canManageStacks = useAuthorizations([
|
||||||
'PortainerStackCreate',
|
'PortainerStackCreate',
|
||||||
'PortainerStackDelete',
|
'PortainerStackDelete',
|
||||||
|
@ -58,7 +58,7 @@ export function StacksDatatable({
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataset={dataset}
|
dataset={dataset}
|
||||||
isRowSelectable={({ original: item }) =>
|
isRowSelectable={({ original: item }) =>
|
||||||
allowSelection(item, isAdmin, canManageStacks)
|
allowSelection(item, isAdminQuery.isAdmin, canManageStacks.authorized)
|
||||||
}
|
}
|
||||||
getRowId={(item) => item.Id.toString()}
|
getRowId={(item) => item.Id.toString()}
|
||||||
initialTableState={{
|
initialTableState={{
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { CellContext, Column } from '@tanstack/react-table';
|
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 { getValueAsArrayOfStrings } from '@/portainer/helpers/array';
|
||||||
import { StackStatus } from '@/react/common/stacks/types';
|
import { StackStatus } from '@/react/common/stacks/types';
|
||||||
import {
|
import {
|
||||||
|
@ -67,7 +67,7 @@ function NameCell({
|
||||||
}
|
}
|
||||||
|
|
||||||
function NameLink({ item }: { item: DecoratedStack }) {
|
function NameLink({ item }: { item: DecoratedStack }) {
|
||||||
const { isAdmin } = useCurrentUser();
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
|
|
||||||
const name = item.Name;
|
const name = item.Name;
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ function NameLink({ item }: { item: DecoratedStack }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAdmin && isOrphanedStack(item)) {
|
if (!isAdminQuery.isAdmin && isOrphanedStack(item)) {
|
||||||
return <>{name}</>;
|
return <>{name}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
import { useDeleteEnvironmentsMutation } from '@/react/portainer/environments/queries/useDeleteEnvironmentsMutation';
|
import { useDeleteEnvironmentsMutation } from '@/react/portainer/environments/queries/useDeleteEnvironmentsMutation';
|
||||||
import { Environment } from '@/react/portainer/environments/types';
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||||
|
import { useIsPureAdmin } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
import { ModalType, openModal } from '@@/modals';
|
import { ModalType, openModal } from '@@/modals';
|
||||||
|
@ -28,6 +29,7 @@ export function TableActions({
|
||||||
}: {
|
}: {
|
||||||
selectedRows: WaitingRoomEnvironment[];
|
selectedRows: WaitingRoomEnvironment[];
|
||||||
}) {
|
}) {
|
||||||
|
const isPureAdmin = useIsPureAdmin();
|
||||||
const associateMutation = useAssociateDeviceMutation();
|
const associateMutation = useAssociateDeviceMutation();
|
||||||
const removeMutation = useDeleteEnvironmentsMutation();
|
const removeMutation = useDeleteEnvironmentsMutation();
|
||||||
const licenseOverused = useLicenseOverused(selectedRows.length);
|
const licenseOverused = useLicenseOverused(selectedRows.length);
|
||||||
|
@ -58,7 +60,9 @@ export function TableActions({
|
||||||
<span>
|
<span>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleAssociateAndAssign(selectedRows)}
|
onClick={() => handleAssociateAndAssign(selectedRows)}
|
||||||
disabled={selectedRows.length === 0 || licenseOverused}
|
disabled={
|
||||||
|
selectedRows.length === 0 || licenseOverused || !isPureAdmin
|
||||||
|
}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
icon={CheckCircle}
|
icon={CheckCircle}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,13 +1,10 @@
|
||||||
import { useRouter } from '@uirouter/react';
|
import { useRouter } from '@uirouter/react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { EnvironmentId } from '../portainer/environments/types';
|
|
||||||
|
|
||||||
import { useAuthorizations } from './useUser';
|
import { useAuthorizations } from './useUser';
|
||||||
|
|
||||||
type AuthorizationOptions = {
|
type AuthorizationOptions = {
|
||||||
authorizations: string | string[];
|
authorizations: string | string[];
|
||||||
forceEnvironmentId?: EnvironmentId;
|
|
||||||
adminOnlyCE?: boolean;
|
adminOnlyCE?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -19,24 +16,19 @@ type RedirectOptions = {
|
||||||
/**
|
/**
|
||||||
* Redirects to the given route if the user is not authorized.
|
* Redirects to the given route if the user is not authorized.
|
||||||
* @param authorizations The authorizations to check.
|
* @param authorizations The authorizations to check.
|
||||||
* @param forceEnvironmentId The environment id to use for the check.
|
* @param adminOnlyCE Whether to allow non-admin users in CE.
|
||||||
* @param adminOnlyCE Whether to check only for admin authorizations in CE.
|
|
||||||
* @param to The route to redirect to.
|
* @param to The route to redirect to.
|
||||||
* @param params The params to pass to the route.
|
* @param params The params to pass to the route.
|
||||||
*/
|
*/
|
||||||
export function useUnauthorizedRedirect(
|
export function useUnauthorizedRedirect(
|
||||||
{
|
{ authorizations, adminOnlyCE = false }: AuthorizationOptions,
|
||||||
authorizations,
|
|
||||||
forceEnvironmentId,
|
|
||||||
adminOnlyCE = false,
|
|
||||||
}: AuthorizationOptions,
|
|
||||||
{ to, params }: RedirectOptions
|
{ to, params }: RedirectOptions
|
||||||
) {
|
) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const isAuthorized = useAuthorizations(
|
const isAuthorized = useAuthorizations(
|
||||||
authorizations,
|
authorizations,
|
||||||
forceEnvironmentId,
|
undefined,
|
||||||
adminOnlyCE
|
adminOnlyCE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,14 @@ import {
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
} from 'react';
|
} 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 { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
import { User } from '@/portainer/users/types';
|
import { User } from '@/portainer/users/types';
|
||||||
import { useLoadCurrentUser } from '@/portainer/users/queries/useLoadCurrentUser';
|
import { useLoadCurrentUser } from '@/portainer/users/queries/useLoadCurrentUser';
|
||||||
|
|
||||||
|
import { useEnvironment } from '../portainer/environments/queries';
|
||||||
|
import { isBE } from '../portainer/feature-flags/feature-flags.service';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
user?: User;
|
user?: User;
|
||||||
}
|
}
|
||||||
|
@ -39,32 +42,84 @@ export function useCurrentUser() {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
user,
|
user,
|
||||||
isAdmin: isAdmin(user),
|
isPureAdmin: isPureAdmin(user),
|
||||||
}),
|
}),
|
||||||
[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(
|
export function useAuthorizations(
|
||||||
authorizations: string | string[],
|
authorizations: string | string[],
|
||||||
forceEnvironmentId?: EnvironmentId,
|
forceEnvironmentId?: EnvironmentId,
|
||||||
adminOnlyCE = false
|
adminOnlyCE = false
|
||||||
) {
|
) {
|
||||||
const { user } = useUser();
|
const { user } = useCurrentUser();
|
||||||
const {
|
const {
|
||||||
params: { endpointId },
|
params: { endpointId },
|
||||||
} = useCurrentStateAndParams();
|
} = useCurrentStateAndParams();
|
||||||
|
const envQuery = useEnvironment(forceEnvironmentId || endpointId);
|
||||||
|
const isAdmin = useIsEdgeAdmin({ forceEnvironmentId });
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false;
|
return { authorized: false, isLoading: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
return hasAuthorizations(
|
if (envQuery.isLoading) {
|
||||||
user,
|
return { authorized: false, isLoading: true };
|
||||||
authorizations,
|
}
|
||||||
forceEnvironmentId || endpointId,
|
|
||||||
adminOnlyCE
|
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({
|
export function useIsEnvironmentAdmin({
|
||||||
|
@ -81,24 +136,18 @@ export function useIsEnvironmentAdmin({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isEnvironmentAdmin(
|
/**
|
||||||
user: User,
|
* will return true if the user has the authorizations. assumes the user is authenticated and not an admin
|
||||||
environmentId: EnvironmentId,
|
* @param user
|
||||||
adminOnlyCE = true
|
* @param authorizations
|
||||||
) {
|
* @param environmentId
|
||||||
return hasAuthorizations(
|
* @param adminOnlyCE
|
||||||
user,
|
* @returns
|
||||||
['EndpointResourcesAccess'],
|
*/
|
||||||
environmentId,
|
function hasAuthorizations(
|
||||||
adminOnlyCE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasAuthorizations(
|
|
||||||
user: User,
|
user: User,
|
||||||
authorizations: string | string[],
|
authorizations: string | string[],
|
||||||
environmentId?: EnvironmentId,
|
environmentId?: EnvironmentId
|
||||||
adminOnlyCE = false
|
|
||||||
) {
|
) {
|
||||||
const authorizationsArray =
|
const authorizationsArray =
|
||||||
typeof authorizations === 'string' ? [authorizations] : authorizations;
|
typeof authorizations === 'string' ? [authorizations] : authorizations;
|
||||||
|
@ -107,26 +156,13 @@ export function hasAuthorizations(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.PORTAINER_EDITION === 'CE') {
|
|
||||||
return !adminOnlyCE || isAdmin(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!environmentId) {
|
if (!environmentId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAdmin(user)) {
|
const userEndpointAuthorizations =
|
||||||
return true;
|
user.EndpointAuthorizations?.[environmentId] || [];
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!user.EndpointAuthorizations ||
|
|
||||||
!user.EndpointAuthorizations[environmentId]
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userEndpointAuthorizations = user.EndpointAuthorizations[environmentId];
|
|
||||||
return authorizationsArray.some(
|
return authorizationsArray.some(
|
||||||
(authorization) => userEndpointAuthorizations[authorization]
|
(authorization) => userEndpointAuthorizations[authorization]
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
@ -22,11 +22,14 @@ export function StackName({
|
||||||
inputClassName,
|
inputClassName,
|
||||||
textTip = "Enter or select a 'stack' name to group multiple deployments together, or else leave empty to ignore.",
|
textTip = "Enter or select a 'stack' name to group multiple deployments together, or else leave empty to ignore.",
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { isAdmin } = useCurrentUser();
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
const stackResults = useMemo(
|
const stackResults = useMemo(
|
||||||
() => stacks.filter((stack) => stack.includes(stackName ?? '')),
|
() => stacks.filter((stack) => stack.includes(stackName ?? '')),
|
||||||
[stacks, stackName]
|
[stacks, stackName]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { isAdmin } = isAdminQuery;
|
||||||
|
|
||||||
const tooltip = (
|
const tooltip = (
|
||||||
<>
|
<>
|
||||||
You may specify a stack name to label resources that you want to group.
|
You may specify a stack name to label resources that you want to group.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Plus, RefreshCw } from 'lucide-react';
|
import { Plus, RefreshCw } from 'lucide-react';
|
||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors } from 'formik';
|
||||||
|
|
||||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
|
||||||
import { useEnvironment } from '@/react/portainer/environments/queries';
|
import { useEnvironment } from '@/react/portainer/environments/queries';
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
|
@ -39,7 +39,8 @@ export function LoadBalancerServicesForm({
|
||||||
namespace,
|
namespace,
|
||||||
isEditMode,
|
isEditMode,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { isAdmin } = useCurrentUser();
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
|
|
||||||
const environmentId = useEnvironmentId();
|
const environmentId = useEnvironmentId();
|
||||||
const { data: loadBalancerEnabled, ...loadBalancerEnabledQuery } =
|
const { data: loadBalancerEnabled, ...loadBalancerEnabledQuery } =
|
||||||
useEnvironment(
|
useEnvironment(
|
||||||
|
@ -47,6 +48,12 @@ export function LoadBalancerServicesForm({
|
||||||
(environment) => environment?.Kubernetes.Configuration.UseLoadBalancer
|
(environment) => environment?.Kubernetes.Configuration.UseLoadBalancer
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isAdminQuery.isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isAdmin } = isAdminQuery;
|
||||||
|
|
||||||
const loadBalancerServiceCount = services.filter(
|
const loadBalancerServiceCount = services.filter(
|
||||||
(service) => service.Type === 'LoadBalancer'
|
(service) => service.Type === 'LoadBalancer'
|
||||||
).length;
|
).length;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors } from 'formik';
|
||||||
|
|
||||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { SwitchField } from '@@/form-components/SwitchField';
|
import { SwitchField } from '@@/form-components/SwitchField';
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
@ -113,7 +113,13 @@ export function AutoScalingFormSection({
|
||||||
}
|
}
|
||||||
|
|
||||||
function NoMetricsServerWarning() {
|
function NoMetricsServerWarning() {
|
||||||
const { isAdmin } = useCurrentUser();
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
|
if (isAdminQuery.isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isAdmin } = isAdminQuery;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextTip color="orange">
|
<TextTip color="orange">
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
|
|
|
@ -12,7 +12,6 @@ export function ConfigureView() {
|
||||||
useUnauthorizedRedirect(
|
useUnauthorizedRedirect(
|
||||||
{
|
{
|
||||||
authorizations: 'K8sClusterW',
|
authorizations: 'K8sClusterW',
|
||||||
forceEnvironmentId: environment?.Id,
|
|
||||||
adminOnlyCE: false,
|
adminOnlyCE: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -12,7 +12,6 @@ export function CreateNamespaceView() {
|
||||||
useUnauthorizedRedirect(
|
useUnauthorizedRedirect(
|
||||||
{
|
{
|
||||||
authorizations: 'K8sResourcePoolsW',
|
authorizations: 'K8sResourcePoolsW',
|
||||||
forceEnvironmentId: environmentId,
|
|
||||||
adminOnlyCE: !isBE,
|
adminOnlyCE: !isBE,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -19,13 +19,13 @@ export function RegistriesSelector({
|
||||||
options = [],
|
options = [],
|
||||||
inputId,
|
inputId,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { isAdmin } = useCurrentUser();
|
const { isPureAdmin } = useCurrentUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{options.length === 0 && (
|
{options.length === 0 && (
|
||||||
<p className="text-muted text-xs mb-1 mt-2">
|
<p className="text-muted text-xs mb-1 mt-2">
|
||||||
{isAdmin ? (
|
{isPureAdmin ? (
|
||||||
<span>
|
<span>
|
||||||
No registries available. Head over to the{' '}
|
No registries available. Head over to the{' '}
|
||||||
<Link to="portainer.registries" target="_blank">
|
<Link to="portainer.registries" target="_blank">
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Edit2, Settings } from 'lucide-react';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { useUser } from '@/react/hooks/useUser';
|
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||||
import {
|
import {
|
||||||
Environment,
|
Environment,
|
||||||
PlatformType,
|
PlatformType,
|
||||||
|
@ -15,7 +15,7 @@ import {
|
||||||
import { LinkButton } from '@@/LinkButton';
|
import { LinkButton } from '@@/LinkButton';
|
||||||
|
|
||||||
export function EditButtons({ environment }: { environment: Environment }) {
|
export function EditButtons({ environment }: { environment: Environment }) {
|
||||||
const { isAdmin } = useUser();
|
const { isPureAdmin } = useCurrentUser();
|
||||||
|
|
||||||
const isEdgeAsync = checkEdgeAsync(environment);
|
const isEdgeAsync = checkEdgeAsync(environment);
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ export function EditButtons({ environment }: { environment: Environment }) {
|
||||||
return (
|
return (
|
||||||
<ButtonsGrid className="ml-3 w-11">
|
<ButtonsGrid className="ml-3 w-11">
|
||||||
<LinkButton
|
<LinkButton
|
||||||
disabled={!isAdmin}
|
disabled={!isPureAdmin}
|
||||||
to="portainer.endpoints.endpoint"
|
to="portainer.endpoints.endpoint"
|
||||||
params={{ id: environment.Id, redirectTo: 'portainer.home' }}
|
params={{ id: environment.Id, redirectTo: 'portainer.home' }}
|
||||||
color="none"
|
color="none"
|
||||||
|
@ -42,7 +42,7 @@ export function EditButtons({ environment }: { environment: Environment }) {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LinkButton
|
<LinkButton
|
||||||
disabled={!configRoute || isEdgeAsync || !isAdmin}
|
disabled={!configRoute || isEdgeAsync || !isPureAdmin}
|
||||||
to={configRoute}
|
to={configRoute}
|
||||||
params={{ endpointId: environment.Id }}
|
params={{ endpointId: environment.Id }}
|
||||||
color="none"
|
color="none"
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
} from '@/react/portainer/environments/queries/useEnvironmentList';
|
} from '@/react/portainer/environments/queries/useEnvironmentList';
|
||||||
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
||||||
import { EnvironmentsQueryParams } from '@/react/portainer/environments/environment.service';
|
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 { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
|
||||||
import { environmentStore } from '@/react/hooks/current-environment-store';
|
import { environmentStore } from '@/react/hooks/current-environment-store';
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ interface Props {
|
||||||
const storageKey = 'home_endpoints';
|
const storageKey = 'home_endpoints';
|
||||||
|
|
||||||
export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
|
export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
|
||||||
const { isAdmin } = useUser();
|
const isPureAdmin = useIsPureAdmin();
|
||||||
const currentEnvStore = useStore(environmentStore);
|
const currentEnvStore = useStore(environmentStore);
|
||||||
|
|
||||||
const [platformTypes, setPlatformTypes] = useHomePageFilter<PlatformType[]>(
|
const [platformTypes, setPlatformTypes] = useHomePageFilter<PlatformType[]>(
|
||||||
|
@ -138,7 +138,9 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{totalAvailable === 0 && <NoEnvironmentsInfoPanel isAdmin={isAdmin} />}
|
{totalAvailable === 0 && (
|
||||||
|
<NoEnvironmentsInfoPanel isAdmin={isPureAdmin} />
|
||||||
|
)}
|
||||||
|
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
|
@ -160,7 +162,7 @@ export function EnvironmentList({ onClickBrowse, onRefresh }: Props) {
|
||||||
placeholder="Search by name, group, tag, status, URL..."
|
placeholder="Search by name, group, tag, status, URL..."
|
||||||
data-cy="home-endpointsSearchInput"
|
data-cy="home-endpointsSearchInput"
|
||||||
/>
|
/>
|
||||||
{isAdmin && (
|
{isPureAdmin && (
|
||||||
<Button
|
<Button
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
data-cy="home-refreshEndpointsButton"
|
data-cy="home-refreshEndpointsButton"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors } from 'formik';
|
||||||
|
|
||||||
import { useUser } from '@/react/hooks/useUser';
|
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
|
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
|
||||||
import { SwitchField } from '@@/form-components/SwitchField';
|
import { SwitchField } from '@@/form-components/SwitchField';
|
||||||
|
@ -26,7 +26,13 @@ export function AccessControlForm({
|
||||||
errors,
|
errors,
|
||||||
environmentId,
|
environmentId,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { isAdmin } = useUser();
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
|
|
||||||
|
if (isAdminQuery.isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isAdmin } = isAdminQuery;
|
||||||
|
|
||||||
const accessControlEnabled =
|
const accessControlEnabled =
|
||||||
values.ownership !== ResourceControlOwnership.PUBLIC;
|
values.ownership !== ResourceControlOwnership.PUBLIC;
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import { useReducer } from 'react';
|
import { useReducer } from 'react';
|
||||||
import { Edit, Eye } from 'lucide-react';
|
import { Edit, Eye } from 'lucide-react';
|
||||||
|
|
||||||
import { useUser } from '@/react/hooks/useUser';
|
|
||||||
import { Icon } from '@/react/components/Icon';
|
import { Icon } from '@/react/components/Icon';
|
||||||
import { TeamMembership, TeamRole } from '@/react/portainer/users/teams/types';
|
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 { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { TableContainer, TableTitle } from '@@/datatables';
|
import { TableContainer, TableTitle } from '@@/datatables';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
|
@ -34,20 +37,27 @@ export function AccessControlPanel({
|
||||||
onUpdateSuccess,
|
onUpdateSuccess,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [isEditMode, toggleEditMode] = useReducer((state) => !state, false);
|
const [isEditMode, toggleEditMode] = useReducer((state) => !state, false);
|
||||||
const { user, isAdmin } = useUser();
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
|
const isTeamLeader = useIsCurrentUserTeamLeader();
|
||||||
|
|
||||||
const isInherited = checkIfInherited();
|
const isInherited = checkIfInherited();
|
||||||
|
|
||||||
|
const restrictions = useRestrictions(resourceControl);
|
||||||
|
|
||||||
|
if (isAdminQuery.isLoading || !restrictions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const { isPartOfRestrictedUsers, isLeaderOfAnyRestrictedTeams } =
|
const { isPartOfRestrictedUsers, isLeaderOfAnyRestrictedTeams } =
|
||||||
useRestrictions(resourceControl);
|
restrictions;
|
||||||
|
|
||||||
|
const { isAdmin } = isAdminQuery;
|
||||||
|
|
||||||
const isEditDisabled =
|
const isEditDisabled =
|
||||||
disableOwnershipChange ||
|
disableOwnershipChange ||
|
||||||
isInherited ||
|
isInherited ||
|
||||||
(!isAdmin && !isPartOfRestrictedUsers && !isLeaderOfAnyRestrictedTeams);
|
(!isAdmin && !isPartOfRestrictedUsers && !isLeaderOfAnyRestrictedTeams);
|
||||||
|
|
||||||
const isTeamLeader = useIsTeamLeader(user) as boolean;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<TableTitle label="Access control" icon={Eye} />
|
<TableTitle label="Access control" icon={Eye} />
|
||||||
|
@ -106,10 +116,16 @@ export function AccessControlPanel({
|
||||||
}
|
}
|
||||||
|
|
||||||
function useRestrictions(resourceControl?: ResourceControlViewModel) {
|
function useRestrictions(resourceControl?: ResourceControlViewModel) {
|
||||||
const { user, isAdmin } = useUser();
|
const { user } = useCurrentUser();
|
||||||
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
const memberships = useUserMembership(user.Id);
|
const memberships = useUserMembership(user.Id);
|
||||||
|
|
||||||
|
if (isAdminQuery.isLoading) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isAdmin } = isAdminQuery;
|
||||||
|
|
||||||
if (!resourceControl || isAdmin) {
|
if (!resourceControl || isAdmin) {
|
||||||
return {
|
return {
|
||||||
isPartOfRestrictedUsers: false,
|
isPartOfRestrictedUsers: false,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import clsx from 'clsx';
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
import { object } from 'yup';
|
import { object } from 'yup';
|
||||||
|
|
||||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
import { useCurrentUser, useIsEdgeAdmin } from '@/react/hooks/useUser';
|
||||||
import { notifySuccess } from '@/portainer/services/notifications';
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
|
@ -43,7 +43,8 @@ export function AccessControlPanelForm({
|
||||||
onCancelClick,
|
onCancelClick,
|
||||||
onUpdateSuccess,
|
onUpdateSuccess,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { isAdmin, user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
|
|
||||||
const updateAccess = useMutation(
|
const updateAccess = useMutation(
|
||||||
(variables: AccessControlFormData) =>
|
(variables: AccessControlFormData) =>
|
||||||
|
@ -63,6 +64,12 @@ export function AccessControlPanelForm({
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isAdminQuery.isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isAdmin } = isAdminQuery;
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
accessControl: parseAccessControlFormData(
|
accessControl: parseAccessControlFormData(
|
||||||
isAdmin,
|
isAdmin,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { FormikErrors } from 'formik';
|
import { FormikErrors } from 'formik';
|
||||||
|
|
||||||
import { useUser } from '@/react/hooks/useUser';
|
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
import { FormError } from '@@/form-components/FormError';
|
import { FormError } from '@@/form-components/FormError';
|
||||||
|
@ -30,9 +30,10 @@ export function EditDetails({
|
||||||
formNamespace,
|
formNamespace,
|
||||||
environmentId,
|
environmentId,
|
||||||
}: Props) {
|
}: 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(
|
const handleChange = useCallback(
|
||||||
(partialValues: Partial<typeof values>) => {
|
(partialValues: Partial<typeof values>) => {
|
||||||
onChange({ ...values, ...partialValues });
|
onChange({ ...values, ...partialValues });
|
||||||
|
@ -41,7 +42,12 @@ export function EditDetails({
|
||||||
[values, onChange]
|
[values, onChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading || !teams || (isAdmin && !users) || !values.authorizedUsers) {
|
if (
|
||||||
|
isLoading ||
|
||||||
|
!teams ||
|
||||||
|
(isPureAdmin && !users) ||
|
||||||
|
!values.authorizedUsers
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,14 +57,14 @@ export function EditDetails({
|
||||||
onChange={handleChangeOwnership}
|
onChange={handleChangeOwnership}
|
||||||
name={withNamespace('ownership')}
|
name={withNamespace('ownership')}
|
||||||
value={values.ownership}
|
value={values.ownership}
|
||||||
isAdmin={isAdmin}
|
isAdmin={isPureAdmin}
|
||||||
isPublicVisible={isPublicVisible}
|
isPublicVisible={isPublicVisible}
|
||||||
teams={teams}
|
teams={teams}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{values.ownership === ResourceControlOwnership.RESTRICTED && (
|
{values.ownership === ResourceControlOwnership.RESTRICTED && (
|
||||||
<div aria-label="extra-options">
|
<div aria-label="extra-options">
|
||||||
{isAdmin && (
|
{isPureAdmin && (
|
||||||
<UsersField
|
<UsersField
|
||||||
name={withNamespace('authorizedUsers')}
|
name={withNamespace('authorizedUsers')}
|
||||||
users={users || []}
|
users={users || []}
|
||||||
|
@ -68,12 +74,12 @@ export function EditDetails({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(isAdmin || teams.length > 1) && (
|
{(isPureAdmin || teams.length > 1) && (
|
||||||
<TeamsField
|
<TeamsField
|
||||||
name={withNamespace('authorizedTeams')}
|
name={withNamespace('authorizedTeams')}
|
||||||
teams={teams}
|
teams={teams}
|
||||||
overrideTooltip={
|
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.'
|
? 'As you are a member of multiple teams, you can select which teams(s) will be able to manage this resource.'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
|
@ -111,7 +117,7 @@ export function EditDetails({
|
||||||
// Non admin team leaders/members under only one team can
|
// Non admin team leaders/members under only one team can
|
||||||
// automatically grant the resource access to all members
|
// automatically grant the resource access to all members
|
||||||
// under the team
|
// under the team
|
||||||
if (!isAdmin && teams && teams.length === 1) {
|
if (!isPureAdmin && teams && teams.length === 1) {
|
||||||
authorizedTeams = teams.map((team) => team.Id);
|
authorizedTeams = teams.map((team) => team.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
import { useTeams } from '@/react/portainer/users/teams/queries';
|
import { useTeams } from '@/react/portainer/users/teams/queries';
|
||||||
import { useUsers } from '@/portainer/users/queries';
|
import { useUsers } from '@/portainer/users/queries';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
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 teams = useTeams(false, environmentId);
|
||||||
|
|
||||||
const users = useUsers(false, environmentId, enabled);
|
const users = useUsers(false, environmentId, isAdminQuery.isAdmin);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
teams: teams.data,
|
teams: teams.data,
|
||||||
users: users.data,
|
users: users.data,
|
||||||
isLoading: teams.isLoading || users.isLoading,
|
isAdmin: isAdminQuery.isAdmin,
|
||||||
|
isLoading: teams.isLoading || users.isLoading || isAdminQuery.isLoading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,14 @@ import { Environment, EnvironmentId } from '../types';
|
||||||
|
|
||||||
import { environmentQueryKeys } from './query-keys';
|
import { environmentQueryKeys } from './query-keys';
|
||||||
|
|
||||||
export function useEnvironment<T = Environment | null>(
|
export function useEnvironment<T = Environment>(
|
||||||
environmentId?: EnvironmentId,
|
environmentId?: EnvironmentId,
|
||||||
select?: (environment: Environment | null) => T,
|
select?: (environment: Environment) => T,
|
||||||
options?: { autoRefreshRate?: number }
|
options?: { autoRefreshRate?: number }
|
||||||
) {
|
) {
|
||||||
return useQuery(
|
return useQuery(
|
||||||
environmentId ? environmentQueryKeys.item(environmentId) : [],
|
environmentQueryKeys.item(environmentId!),
|
||||||
() => (environmentId ? getEndpoint(environmentId) : null),
|
() => getEndpoint(environmentId!),
|
||||||
{
|
{
|
||||||
select,
|
select,
|
||||||
...withError('Failed loading environment'),
|
...withError('Failed loading environment'),
|
||||||
|
|
|
@ -1,24 +1,27 @@
|
||||||
import { useField } from 'formik';
|
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 { TagSelector } from '@@/TagSelector';
|
||||||
import { FormSection } from '@@/form-components/FormSection';
|
import { FormSection } from '@@/form-components/FormSection';
|
||||||
|
|
||||||
import { GroupField } from './GroupsField';
|
import { GroupField } from './GroupsField';
|
||||||
|
|
||||||
export function MetadataFieldset() {
|
export function MetadataFieldset({ children }: PropsWithChildren<unknown>) {
|
||||||
const [tagProps, , tagHelpers] = useField('meta.tagIds');
|
const [tagProps, , tagHelpers] = useField('meta.tagIds');
|
||||||
|
|
||||||
const { isAdmin } = useUser();
|
const { isPureAdmin } = useCurrentUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormSection title="Metadata">
|
<FormSection title="Metadata">
|
||||||
|
{children}
|
||||||
|
|
||||||
<GroupField />
|
<GroupField />
|
||||||
|
|
||||||
<TagSelector
|
<TagSelector
|
||||||
value={tagProps.value}
|
value={tagProps.value}
|
||||||
allowCreate={isAdmin}
|
allowCreate={isPureAdmin}
|
||||||
onChange={(value) => tagHelpers.setValue(value)}
|
onChange={(value) => tagHelpers.setValue(value)}
|
||||||
/>
|
/>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCurrentStateAndParams } from '@uirouter/react';
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
import { parseAccessControlFormData } from '@/react/portainer/access-control/utils';
|
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 { StackType } from '@/react/common/stacks/types';
|
||||||
|
|
||||||
import { Platform } from '../../types';
|
import { Platform } from '../../types';
|
||||||
|
@ -19,11 +19,13 @@ export function useInitialValues({
|
||||||
isEdge?: boolean;
|
isEdge?: boolean;
|
||||||
buildMethods: Array<Method>;
|
buildMethods: Array<Method>;
|
||||||
}): FormValues | undefined {
|
}): FormValues | undefined {
|
||||||
const { user, isAdmin } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
|
|
||||||
const { appTemplateId, type = defaultType } = useAppTemplateParams();
|
const { appTemplateId, type = defaultType } = useAppTemplateParams();
|
||||||
|
|
||||||
const fileContentQuery = useFetchTemplateFile(appTemplateId);
|
const fileContentQuery = useFetchTemplateFile(appTemplateId);
|
||||||
if (fileContentQuery.isLoading) {
|
if (fileContentQuery.isLoading || isAdminQuery.isLoading) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +53,7 @@ export function useInitialValues({
|
||||||
},
|
},
|
||||||
AccessControl: isEdge
|
AccessControl: isEdge
|
||||||
? undefined
|
? undefined
|
||||||
: parseAccessControlFormData(isAdmin, user.Id),
|
: parseAccessControlFormData(isAdminQuery.isAdmin, user.Id),
|
||||||
EdgeSettings: isEdge ? getDefaultEdgeTemplateSettings() : undefined,
|
EdgeSettings: isEdge ? getDefaultEdgeTemplateSettings() : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ export function EditForm({
|
||||||
templateFile: fileContentQuery.data,
|
templateFile: fileContentQuery.data,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (fileContentQuery.isLoading) {
|
if (fileContentQuery.isLoading || !initialValues) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { parseAccessControlFormData } from '@/react/portainer/access-control/utils';
|
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 { toGitFormModel } from '@/react/portainer/gitops/types';
|
||||||
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
import { ResourceControlViewModel } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||||
|
|
||||||
|
@ -15,8 +15,14 @@ export function useInitialValues({
|
||||||
template: CustomTemplate;
|
template: CustomTemplate;
|
||||||
templateFile: string | undefined;
|
templateFile: string | undefined;
|
||||||
isEdge: boolean;
|
isEdge: boolean;
|
||||||
}): FormValues {
|
}): FormValues | undefined {
|
||||||
const { user, isAdmin } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
|
|
||||||
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
|
|
||||||
|
if (isAdminQuery.isLoading) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
Title: template.Title,
|
Title: template.Title,
|
||||||
|
@ -31,7 +37,7 @@ export function useInitialValues({
|
||||||
AccessControl:
|
AccessControl:
|
||||||
!isEdge && template.ResourceControl
|
!isEdge && template.ResourceControl
|
||||||
? parseAccessControlFormData(
|
? parseAccessControlFormData(
|
||||||
isAdmin,
|
isAdminQuery.isAdmin,
|
||||||
user.Id,
|
user.Id,
|
||||||
new ResourceControlViewModel(template.ResourceControl)
|
new ResourceControlViewModel(template.ResourceControl)
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Edit, Trash2 } from 'lucide-react';
|
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 { StackType } from '@/react/common/stacks/types';
|
||||||
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
|
import { CustomTemplate } from '@/react/portainer/templates/custom-templates/types';
|
||||||
|
|
||||||
|
@ -22,8 +22,15 @@ export function CustomTemplatesListItem({
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
linkParams?: { to: string; params: object };
|
linkParams?: { to: string; params: object };
|
||||||
}) {
|
}) {
|
||||||
const { isAdmin, user } = useCurrentUser();
|
const { user } = useCurrentUser();
|
||||||
const isEditAllowed = isAdmin || template.CreatedByUserId === user.Id;
|
const isAdminQuery = useIsEdgeAdmin();
|
||||||
|
|
||||||
|
if (isAdminQuery.isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEditAllowed =
|
||||||
|
isAdminQuery.isAdmin || template.CreatedByUserId === user.Id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TemplateItem
|
<TemplateItem
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useRouter } from '@uirouter/react';
|
import { useRouter } from '@uirouter/react';
|
||||||
|
|
||||||
import { useUsers } from '@/portainer/users/queries';
|
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 { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||||
|
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
|
@ -16,7 +16,7 @@ import { useTeamIdParam } from './useTeamIdParam';
|
||||||
export function ItemView() {
|
export function ItemView() {
|
||||||
const teamId = useTeamIdParam();
|
const teamId = useTeamIdParam();
|
||||||
|
|
||||||
const { isAdmin } = useUser();
|
const isPureAdmin = useIsPureAdmin();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const teamQuery = useTeam(teamId, () =>
|
const teamQuery = useTeam(teamId, () =>
|
||||||
router.stateService.go('portainer.teams')
|
router.stateService.go('portainer.teams')
|
||||||
|
@ -45,7 +45,7 @@ export function ItemView() {
|
||||||
<Details
|
<Details
|
||||||
team={team}
|
team={team}
|
||||||
memberships={membershipsQuery.data}
|
memberships={membershipsQuery.data}
|
||||||
isAdmin={isAdmin}
|
isAdmin={isPureAdmin}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Users, UserX } from 'lucide-react';
|
||||||
|
|
||||||
import { User, UserId } from '@/portainer/users/types';
|
import { User, UserId } from '@/portainer/users/types';
|
||||||
import { TeamId, TeamRole } from '@/react/portainer/users/teams/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 { notifySuccess } from '@/portainer/services/notifications';
|
||||||
import {
|
import {
|
||||||
useRemoveMemberMutation,
|
useRemoveMemberMutation,
|
||||||
|
@ -37,8 +37,7 @@ export function TeamMembersList({ users, roles, disabled, teamId }: Props) {
|
||||||
{ id: string; desc: boolean } | undefined
|
{ id: string; desc: boolean } | undefined
|
||||||
>({ id: 'name', desc: false });
|
>({ id: 'name', desc: false });
|
||||||
|
|
||||||
const { isAdmin } = useUser();
|
const isPureAdmin = useIsPureAdmin();
|
||||||
|
|
||||||
const rowContext = useMemo<RowContext>(
|
const rowContext = useMemo<RowContext>(
|
||||||
() => ({
|
() => ({
|
||||||
getRole(userId: UserId) {
|
getRole(userId: UserId) {
|
||||||
|
@ -58,7 +57,7 @@ export function TeamMembersList({ users, roles, disabled, teamId }: Props) {
|
||||||
titleIcon={Users}
|
titleIcon={Users}
|
||||||
title="Team members"
|
title="Team members"
|
||||||
renderTableActions={() =>
|
renderTableActions={() =>
|
||||||
isAdmin && (
|
isPureAdmin && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleRemoveMembers(users.map((user) => user.Id))}
|
onClick={() => handleRemoveMembers(users.map((user) => user.Id))}
|
||||||
disabled={disabled || users.length === 0}
|
disabled={disabled || users.length === 0}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { User as UserIcon, UserPlus, UserX } from 'lucide-react';
|
||||||
import { CellContext } from '@tanstack/react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { User } from '@/portainer/users/types';
|
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 { TeamRole } from '@/react/portainer/users/teams/types';
|
||||||
import { notifySuccess } from '@/portainer/services/notifications';
|
import { notifySuccess } from '@/portainer/services/notifications';
|
||||||
import {
|
import {
|
||||||
|
@ -23,7 +23,7 @@ export const teamRole = columnHelper.accessor('Id', {
|
||||||
cell: RoleCell,
|
cell: RoleCell,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function RoleCell({
|
function RoleCell({
|
||||||
row: { original: user },
|
row: { original: user },
|
||||||
getValue,
|
getValue,
|
||||||
}: CellContext<User, User['Id']>) {
|
}: CellContext<User, User['Id']>) {
|
||||||
|
@ -38,12 +38,16 @@ export function RoleCell({
|
||||||
|
|
||||||
const role = getRole(id);
|
const role = getRole(id);
|
||||||
|
|
||||||
const { isAdmin } = useCurrentUser();
|
const { isPureAdmin } = useCurrentUser();
|
||||||
|
|
||||||
const Cell = role === TeamRole.Leader ? LeaderCell : MemberCell;
|
const Cell = role === TeamRole.Leader ? LeaderCell : MemberCell;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Cell isAdmin={isAdmin} onClick={handleUpdateRole} disabled={disabled} />
|
<Cell
|
||||||
|
isAdmin={isPureAdmin}
|
||||||
|
onClick={handleUpdateRole}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
function handleUpdateRole(role: TeamRole, onSuccessMessage: string) {
|
function handleUpdateRole(role: TeamRole, onSuccessMessage: string) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
|
||||||
import { UserPlus, Users } from 'lucide-react';
|
import { UserPlus, Users } from 'lucide-react';
|
||||||
|
|
||||||
import { User, UserId } from '@/portainer/users/types';
|
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 { notifySuccess } from '@/portainer/services/notifications';
|
||||||
import { useAddMemberMutation } from '@/react/portainer/users/teams/queries';
|
import { useAddMemberMutation } from '@/react/portainer/users/teams/queries';
|
||||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
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: string; desc: boolean } | undefined
|
||||||
>({ id: 'name', desc: false });
|
>({ id: 'name', desc: false });
|
||||||
|
|
||||||
const { isAdmin } = useUser();
|
const { isPureAdmin } = useCurrentUser();
|
||||||
|
|
||||||
const rowContext = useMemo(() => ({ disabled, teamId }), [disabled, teamId]);
|
const rowContext = useMemo(() => ({ disabled, teamId }), [disabled, teamId]);
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ export function UsersList({ users, disabled, teamId }: Props) {
|
||||||
titleIcon={Users}
|
titleIcon={Users}
|
||||||
title="Users"
|
title="Users"
|
||||||
renderTableActions={() =>
|
renderTableActions={() =>
|
||||||
isAdmin && (
|
isPureAdmin && (
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleAddAllMembers(users.map((u) => u.Id))}
|
onClick={() => handleAddAllMembers(users.map((u) => u.Id))}
|
||||||
disabled={disabled || users.length === 0}
|
disabled={disabled || users.length === 0}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useUsers } from '@/portainer/users/queries';
|
import { useUsers } from '@/portainer/users/queries';
|
||||||
import { useUser } from '@/react/hooks/useUser';
|
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { PageHeader } from '@@/PageHeader';
|
import { PageHeader } from '@@/PageHeader';
|
||||||
|
|
||||||
|
@ -9,10 +9,10 @@ import { CreateTeamForm } from './CreateTeamForm';
|
||||||
import { TeamsDatatable } from './TeamsDatatable';
|
import { TeamsDatatable } from './TeamsDatatable';
|
||||||
|
|
||||||
export function ListView() {
|
export function ListView() {
|
||||||
const { isAdmin } = useUser();
|
const { isPureAdmin } = useCurrentUser();
|
||||||
|
|
||||||
const usersQuery = useUsers(false);
|
const usersQuery = useUsers(false);
|
||||||
const teamsQuery = useTeams(!isAdmin, 0, { enabled: !!usersQuery.data });
|
const teamsQuery = useTeams(!isPureAdmin, 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -22,12 +22,12 @@ export function ListView() {
|
||||||
reload
|
reload
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isAdmin && usersQuery.data && teamsQuery.data && (
|
{isPureAdmin && usersQuery.data && teamsQuery.data && (
|
||||||
<CreateTeamForm users={usersQuery.data} teams={teamsQuery.data} />
|
<CreateTeamForm users={usersQuery.data} teams={teamsQuery.data} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{teamsQuery.data && (
|
{teamsQuery.data && (
|
||||||
<TeamsDatatable teams={teamsQuery.data} isAdmin={isAdmin} />
|
<TeamsDatatable teams={teamsQuery.data} isAdmin={isPureAdmin} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
type Environment,
|
type Environment,
|
||||||
type EnvironmentId,
|
type EnvironmentId,
|
||||||
} from '@/react/portainer/environments/types';
|
} 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 { useInfo } from '@/react/docker/proxy/queries/useInfo';
|
||||||
import { useApiVersion } from '@/react/docker/proxy/queries/useVersion';
|
import { useApiVersion } from '@/react/docker/proxy/queries/useVersion';
|
||||||
|
|
||||||
|
@ -30,11 +30,11 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DockerSidebar({ environmentId, environment }: Props) {
|
export function DockerSidebar({ environmentId, environment }: Props) {
|
||||||
const { user } = useUser();
|
const isEnvironmentAdmin = useIsEnvironmentAdmin({ adminOnlyCE: true });
|
||||||
const isAdmin = isEnvironmentAdmin(user, environmentId);
|
|
||||||
|
|
||||||
const areStacksVisible =
|
const areStacksVisible =
|
||||||
isAdmin || environment.SecuritySettings.allowStackManagementForRegularUsers;
|
isEnvironmentAdmin ||
|
||||||
|
environment.SecuritySettings.allowStackManagementForRegularUsers;
|
||||||
|
|
||||||
const envInfoQuery = useInfo(
|
const envInfoQuery = useInfo(
|
||||||
environmentId,
|
environmentId,
|
||||||
|
@ -167,7 +167,7 @@ export function DockerSidebar({ environmentId, environment }: Props) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isSwarmManager && isAdmin && (
|
{!isSwarmManager && isEnvironmentAdmin && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="docker.events"
|
to="docker.events"
|
||||||
params={{ endpointId: environmentId }}
|
params={{ endpointId: environmentId }}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import clsx from 'clsx';
|
||||||
|
|
||||||
import { useSystemStatus } from '@/react/portainer/system/useSystemStatus';
|
import { useSystemStatus } from '@/react/portainer/system/useSystemStatus';
|
||||||
import { useSystemVersion } from '@/react/portainer/system/useSystemVersion';
|
import { useSystemVersion } from '@/react/portainer/system/useSystemVersion';
|
||||||
import { useCurrentUser } from '@/react/hooks/useUser';
|
import { useIsEdgeAdmin } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { Modal } from '@@/modals';
|
import { Modal } from '@@/modals';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
|
@ -47,7 +47,7 @@ export function BuildInfoModalButton() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function BuildInfoModal({ closeModal }: { closeModal: () => void }) {
|
function BuildInfoModal({ closeModal }: { closeModal: () => void }) {
|
||||||
const { isAdmin } = useCurrentUser();
|
const { isAdmin } = useIsEdgeAdmin({ noEnvScope: true });
|
||||||
const versionQuery = useSystemVersion();
|
const versionQuery = useSystemVersion();
|
||||||
const statusQuery = useSystemStatus();
|
const statusQuery = useSystemStatus();
|
||||||
|
|
||||||
|
|
|
@ -16,17 +16,19 @@ import { SidebarSection } from './SidebarSection';
|
||||||
import { SidebarParent } from './SidebarItem/SidebarParent';
|
import { SidebarParent } from './SidebarItem/SidebarParent';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
isPureAdmin: boolean;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isTeamLeader?: boolean;
|
isTeamLeader?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
|
export function SettingsSidebar({ isPureAdmin, isAdmin, isTeamLeader }: Props) {
|
||||||
const teamSyncQuery = usePublicSettings<boolean>({
|
const teamSyncQuery = usePublicSettings<boolean>({
|
||||||
select: (settings) => settings.TeamSync,
|
select: (settings) => settings.TeamSync,
|
||||||
});
|
});
|
||||||
|
|
||||||
const showUsersSection =
|
const isPureAdminOrTeamLeader =
|
||||||
!window.ddExtension && (isAdmin || (isTeamLeader && !teamSyncQuery.data));
|
isPureAdmin || (isTeamLeader && !teamSyncQuery.data && !isAdmin);
|
||||||
|
const showUsersSection = !window.ddExtension && isPureAdminOrTeamLeader;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarSection title="Administration">
|
<SidebarSection title="Administration">
|
||||||
|
@ -51,7 +53,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
|
||||||
data-cy="portainerSidebar-teams"
|
data-cy="portainerSidebar-teams"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isAdmin && (
|
{isPureAdmin && (
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.roles"
|
to="portainer.roles"
|
||||||
label="Roles"
|
label="Roles"
|
||||||
|
@ -61,7 +63,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
|
||||||
)}
|
)}
|
||||||
</SidebarParent>
|
</SidebarParent>
|
||||||
)}
|
)}
|
||||||
{isAdmin && (
|
{isPureAdmin && (
|
||||||
<>
|
<>
|
||||||
<SidebarParent
|
<SidebarParent
|
||||||
label="Environment-related"
|
label="Environment-related"
|
||||||
|
@ -74,7 +76,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
|
||||||
'portainer.tags',
|
'portainer.tags',
|
||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
data-cy="k8sSidebar-networking"
|
data-cy="portainerSidebar-environments-area"
|
||||||
>
|
>
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
label="Environments"
|
label="Environments"
|
||||||
|
@ -139,13 +141,24 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
|
||||||
</SidebarParent>
|
</SidebarParent>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{isBE && !isPureAdmin && isAdmin && (
|
||||||
|
<SidebarParent
|
||||||
|
label="Environment-related"
|
||||||
|
icon={HardDrive}
|
||||||
|
to="portainer.endpoints.updateSchedules"
|
||||||
|
data-cy="portainerSidebar-environments-area"
|
||||||
|
>
|
||||||
|
<EdgeUpdatesSidebarItem />
|
||||||
|
</SidebarParent>
|
||||||
|
)}
|
||||||
|
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.notifications"
|
to="portainer.notifications"
|
||||||
icon={Bell}
|
icon={Bell}
|
||||||
label="Notifications"
|
label="Notifications"
|
||||||
data-cy="portainerSidebar-notifications"
|
data-cy="portainerSidebar-notifications"
|
||||||
/>
|
/>
|
||||||
{isAdmin && (
|
{isPureAdmin && (
|
||||||
<SidebarParent
|
<SidebarParent
|
||||||
to="portainer.settings"
|
to="portainer.settings"
|
||||||
label="Settings"
|
label="Settings"
|
||||||
|
@ -158,6 +171,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
|
||||||
isSubMenu
|
isSubMenu
|
||||||
ignorePaths={[
|
ignorePaths={[
|
||||||
'portainer.settings.authentication',
|
'portainer.settings.authentication',
|
||||||
|
'portainer.settings.sharedcredentials',
|
||||||
'portainer.settings.edgeCompute',
|
'portainer.settings.edgeCompute',
|
||||||
]}
|
]}
|
||||||
data-cy="portainerSidebar-generalSettings"
|
data-cy="portainerSidebar-generalSettings"
|
||||||
|
@ -178,6 +192,7 @@ export function SettingsSidebar({ isAdmin, isTeamLeader }: Props) {
|
||||||
data-cy="portainerSidebar-cloud"
|
data-cy="portainerSidebar-cloud"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SidebarItem
|
<SidebarItem
|
||||||
to="portainer.settings.edgeCompute"
|
to="portainer.settings.edgeCompute"
|
||||||
label="Edge Compute"
|
label="Edge Compute"
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { Home } from 'lucide-react';
|
import { Home } from 'lucide-react';
|
||||||
|
|
||||||
import { useUser } from '@/react/hooks/useUser';
|
import { useIsEdgeAdmin, useIsPureAdmin } from '@/react/hooks/useUser';
|
||||||
import { useIsTeamLeader } from '@/portainer/users/queries';
|
import { useIsCurrentUserTeamLeader } from '@/portainer/users/queries';
|
||||||
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
import { usePublicSettings } from '@/react/portainer/settings/queries';
|
||||||
|
|
||||||
import styles from './Sidebar.module.css';
|
import styles from './Sidebar.module.css';
|
||||||
|
@ -25,16 +25,19 @@ export function Sidebar() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function InnerSidebar() {
|
function InnerSidebar() {
|
||||||
const { isAdmin, user } = useUser();
|
const isPureAdmin = useIsPureAdmin();
|
||||||
const isTeamLeader = useIsTeamLeader(user) as boolean;
|
const isAdminQuery = useIsEdgeAdmin({ noEnvScope: true });
|
||||||
|
const isTeamLeader = useIsCurrentUserTeamLeader();
|
||||||
const { isOpen } = useSidebarState();
|
const { isOpen } = useSidebarState();
|
||||||
|
|
||||||
const settingsQuery = usePublicSettings();
|
const settingsQuery = usePublicSettings();
|
||||||
|
|
||||||
if (!settingsQuery.data) {
|
if (!settingsQuery.data || isAdminQuery.isLoading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { isAdmin } = isAdminQuery;
|
||||||
|
|
||||||
const { LogoURL } = settingsQuery.data;
|
const { LogoURL } = settingsQuery.data;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -66,7 +69,11 @@ function InnerSidebar() {
|
||||||
/>
|
/>
|
||||||
<EnvironmentSidebar />
|
<EnvironmentSidebar />
|
||||||
{isAdmin && <EdgeComputeSidebar />}
|
{isAdmin && <EdgeComputeSidebar />}
|
||||||
<SettingsSidebar isAdmin={isAdmin} isTeamLeader={isTeamLeader} />
|
<SettingsSidebar
|
||||||
|
isPureAdmin={isPureAdmin}
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
isTeamLeader={isTeamLeader}
|
||||||
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-auto pt-8">
|
<div className="mt-auto pt-8">
|
||||||
|
|
|
@ -29,7 +29,7 @@ const enabledPlatforms: Array<ContainerPlatform> = [
|
||||||
|
|
||||||
function UpgradeBEBanner() {
|
function UpgradeBEBanner() {
|
||||||
const {
|
const {
|
||||||
isAdmin,
|
isPureAdmin,
|
||||||
user: { Id },
|
user: { Id },
|
||||||
} = useCurrentUser();
|
} = useCurrentUser();
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ function UpgradeBEBanner() {
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
trackEvent(
|
trackEvent(
|
||||||
isAdmin ? 'portainer-upgrade-admin' : 'portainer-upgrade-non-admin',
|
isPureAdmin ? 'portainer-upgrade-admin' : 'portainer-upgrade-non-admin',
|
||||||
{
|
{
|
||||||
category: 'portainer',
|
category: 'portainer',
|
||||||
metadata,
|
metadata,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { useUser } from '@/react/hooks/useUser';
|
import { useCurrentUser } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { UploadLicenseDialog } from './UploadLicenseDialog';
|
import { UploadLicenseDialog } from './UploadLicenseDialog';
|
||||||
import { LoadingDialog } from './LoadingDialog';
|
import { LoadingDialog } from './LoadingDialog';
|
||||||
|
@ -10,7 +10,7 @@ import { GetLicenseDialog } from './GetLicenseDialog';
|
||||||
type Step = 'uploadLicense' | 'loading' | 'getLicense';
|
type Step = 'uploadLicense' | 'loading' | 'getLicense';
|
||||||
|
|
||||||
export function UpgradeDialog({ onDismiss }: { onDismiss: () => void }) {
|
export function UpgradeDialog({ onDismiss }: { onDismiss: () => void }) {
|
||||||
const { isAdmin } = useUser();
|
const { isPureAdmin } = useCurrentUser();
|
||||||
const [currentStep, setCurrentStep] = useState<Step>('uploadLicense');
|
const [currentStep, setCurrentStep] = useState<Step>('uploadLicense');
|
||||||
const [isGetLicenseSubmitted, setIsGetLicenseSubmitted] = useState(false);
|
const [isGetLicenseSubmitted, setIsGetLicenseSubmitted] = useState(false);
|
||||||
const component = getDialog();
|
const component = getDialog();
|
||||||
|
@ -18,7 +18,7 @@ export function UpgradeDialog({ onDismiss }: { onDismiss: () => void }) {
|
||||||
return component;
|
return component;
|
||||||
|
|
||||||
function getDialog() {
|
function getDialog() {
|
||||||
if (!isAdmin) {
|
if (!isPureAdmin) {
|
||||||
return <NonAdminUpgradeDialog onDismiss={onDismiss} />;
|
return <NonAdminUpgradeDialog onDismiss={onDismiss} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue