mirror of
https://github.com/portainer/portainer.git
synced 2025-08-04 21:35:23 +02:00
refactor(portainer): move to react [EE-3350] (#7915)
This commit is contained in:
parent
30e23ea5b4
commit
78dcba614d
192 changed files with 200 additions and 211 deletions
8
app/react/hooks/useCurrentEnvironment.ts
Normal file
8
app/react/hooks/useCurrentEnvironment.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { useEnvironment } from '@/react/portainer/environments/queries/useEnvironment';
|
||||
|
||||
import { useEnvironmentId } from './useEnvironmentId';
|
||||
|
||||
export function useCurrentEnvironment() {
|
||||
const id = useEnvironmentId();
|
||||
return useEnvironment(id);
|
||||
}
|
15
app/react/hooks/useDebounce.ts
Normal file
15
app/react/hooks/useDebounce.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useDebounce<T>(value: T, delay = 500): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setDebouncedValue(value), delay);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
}
|
13
app/react/hooks/useEnvironmentId.ts
Normal file
13
app/react/hooks/useEnvironmentId.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||
|
||||
export function useEnvironmentId() {
|
||||
const {
|
||||
params: { endpointId: environmentId },
|
||||
} = useCurrentStateAndParams();
|
||||
|
||||
if (!environmentId) {
|
||||
throw new Error('endpointId url param is required');
|
||||
}
|
||||
|
||||
return environmentId;
|
||||
}
|
46
app/react/hooks/useLocalStorage.ts
Normal file
46
app/react/hooks/useLocalStorage.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { useState, useCallback, useMemo } from 'react';
|
||||
|
||||
const localStoragePrefix = 'portainer';
|
||||
|
||||
export function keyBuilder(key: string) {
|
||||
return `${localStoragePrefix}.${key}`;
|
||||
}
|
||||
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
defaultValue: T,
|
||||
storage = localStorage
|
||||
): [T, (value: T) => void] {
|
||||
const [value, setValue] = useState(get<T>(key, defaultValue, storage));
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value) => {
|
||||
setValue(value);
|
||||
set<T>(key, value, storage);
|
||||
},
|
||||
[key, storage]
|
||||
);
|
||||
|
||||
return useMemo(() => [value, handleChange], [value, handleChange]);
|
||||
}
|
||||
|
||||
export function get<T>(
|
||||
key: string,
|
||||
defaultValue: T,
|
||||
storage = localStorage
|
||||
): T {
|
||||
const value = storage.getItem(keyBuilder(key));
|
||||
if (!value) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
export function set<T>(key: string, value: T, storage = localStorage) {
|
||||
storage.setItem(keyBuilder(key), JSON.stringify(value));
|
||||
}
|
14
app/react/hooks/usePaginationLimitState.ts
Normal file
14
app/react/hooks/usePaginationLimitState.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { useLocalStorage } from './useLocalStorage';
|
||||
|
||||
export function usePaginationLimitState(
|
||||
key: string
|
||||
): [number, (value: number) => void] {
|
||||
const paginationKey = paginationKeyBuilder(key);
|
||||
const [pageLimit, setPageLimit] = useLocalStorage(paginationKey, 10);
|
||||
|
||||
return [pageLimit, setPageLimit];
|
||||
|
||||
function paginationKeyBuilder(key: string) {
|
||||
return `datatable_pagination_${key}`;
|
||||
}
|
||||
}
|
37
app/react/hooks/useUIState.tsx
Normal file
37
app/react/hooks/useUIState.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import create from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/react/hooks/useLocalStorage';
|
||||
|
||||
interface UIState {
|
||||
dismissedInfoPanels: Record<string, boolean>;
|
||||
dismissInfoPanel(id: string): void;
|
||||
|
||||
dismissedInfoHash: string;
|
||||
dismissMotd(hash: string): void;
|
||||
|
||||
dismissedUpdateVersion: string;
|
||||
dismissUpdateVersion(version: string): void;
|
||||
}
|
||||
|
||||
export const useUIState = create<UIState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
dismissedInfoPanels: {},
|
||||
dismissInfoPanel(id: string) {
|
||||
set((state) => ({
|
||||
dismissedInfoPanels: { ...state.dismissedInfoPanels, [id]: true },
|
||||
}));
|
||||
},
|
||||
dismissedInfoHash: '',
|
||||
dismissMotd(hash: string) {
|
||||
set({ dismissedInfoHash: hash });
|
||||
},
|
||||
dismissedUpdateVersion: '',
|
||||
dismissUpdateVersion(version: string) {
|
||||
set({ dismissedUpdateVersion: version });
|
||||
},
|
||||
}),
|
||||
{ name: keyBuilder('NEW_UI_STATE') }
|
||||
)
|
||||
);
|
180
app/react/hooks/useUser.tsx
Normal file
180
app/react/hooks/useUser.tsx
Normal file
|
@ -0,0 +1,180 @@
|
|||
import jwtDecode from 'jwt-decode';
|
||||
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useMemo,
|
||||
PropsWithChildren,
|
||||
} from 'react';
|
||||
|
||||
import { isAdmin } from '@/portainer/users/user.helpers';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { getUser } from '@/portainer/users/user.service';
|
||||
import { User, UserId } from '@/portainer/users/types';
|
||||
|
||||
import { useLocalStorage } from './useLocalStorage';
|
||||
|
||||
interface State {
|
||||
user?: User;
|
||||
}
|
||||
|
||||
export const UserContext = createContext<State | null>(null);
|
||||
UserContext.displayName = 'UserContext';
|
||||
|
||||
export function useUser() {
|
||||
const context = useContext(UserContext);
|
||||
|
||||
if (context === null) {
|
||||
throw new Error('should be nested under UserProvider');
|
||||
}
|
||||
|
||||
const { user } = context;
|
||||
if (typeof user === 'undefined') {
|
||||
throw new Error('should be authenticated');
|
||||
}
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
user,
|
||||
isAdmin: isAdmin(user),
|
||||
}),
|
||||
[user]
|
||||
);
|
||||
}
|
||||
|
||||
export function useAuthorizations(
|
||||
authorizations: string | string[],
|
||||
forceEnvironmentId?: EnvironmentId,
|
||||
adminOnlyCE = false
|
||||
) {
|
||||
const { user } = useUser();
|
||||
const {
|
||||
params: { endpointId },
|
||||
} = useCurrentStateAndParams();
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return hasAuthorizations(
|
||||
user,
|
||||
authorizations,
|
||||
forceEnvironmentId || endpointId,
|
||||
adminOnlyCE
|
||||
);
|
||||
}
|
||||
|
||||
export function isEnvironmentAdmin(
|
||||
user: User,
|
||||
environmentId: EnvironmentId,
|
||||
adminOnlyCE = true
|
||||
) {
|
||||
return hasAuthorizations(
|
||||
user,
|
||||
['EndpointResourcesAccess'],
|
||||
environmentId,
|
||||
adminOnlyCE
|
||||
);
|
||||
}
|
||||
|
||||
export function hasAuthorizations(
|
||||
user: User,
|
||||
authorizations: string | string[],
|
||||
environmentId?: EnvironmentId,
|
||||
adminOnlyCE = false
|
||||
) {
|
||||
const authorizationsArray =
|
||||
typeof authorizations === 'string' ? [authorizations] : authorizations;
|
||||
|
||||
if (authorizationsArray.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (process.env.PORTAINER_EDITION === 'CE') {
|
||||
return !adminOnlyCE || isAdmin(user);
|
||||
}
|
||||
|
||||
if (!environmentId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isAdmin(user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
!user.EndpointAuthorizations ||
|
||||
!user.EndpointAuthorizations[environmentId]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const userEndpointAuthorizations = user.EndpointAuthorizations[environmentId];
|
||||
return authorizationsArray.some(
|
||||
(authorization) => userEndpointAuthorizations[authorization]
|
||||
);
|
||||
}
|
||||
|
||||
interface AuthorizedProps {
|
||||
authorizations: string | string[];
|
||||
environmentId?: EnvironmentId;
|
||||
adminOnlyCE?: boolean;
|
||||
childrenUnauthorized?: ReactNode;
|
||||
}
|
||||
|
||||
export function Authorized({
|
||||
authorizations,
|
||||
environmentId,
|
||||
adminOnlyCE = false,
|
||||
children,
|
||||
childrenUnauthorized = null,
|
||||
}: PropsWithChildren<AuthorizedProps>) {
|
||||
const isAllowed = useAuthorizations(
|
||||
authorizations,
|
||||
environmentId,
|
||||
adminOnlyCE
|
||||
);
|
||||
|
||||
return isAllowed ? <>{children}</> : <>{childrenUnauthorized}</>;
|
||||
}
|
||||
|
||||
interface UserProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function UserProvider({ children }: UserProviderProps) {
|
||||
const [jwt] = useLocalStorage('JWT', '');
|
||||
const [user, setUser] = useState<User>();
|
||||
|
||||
useEffect(() => {
|
||||
if (jwt !== '') {
|
||||
const tokenPayload = jwtDecode(jwt) as { id: number };
|
||||
|
||||
loadUser(tokenPayload.id);
|
||||
}
|
||||
}, [jwt]);
|
||||
|
||||
const providerState = useMemo(() => ({ user }), [user]);
|
||||
|
||||
if (jwt === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!providerState.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={providerState}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
||||
async function loadUser(id: UserId) {
|
||||
const user = await getUser(id);
|
||||
setUser(user);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue