1
0
Fork 0
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:
Chaim Lev-Ari 2022-11-13 10:10:18 +02:00 committed by GitHub
parent 30e23ea5b4
commit 78dcba614d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
192 changed files with 200 additions and 211 deletions

View 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);
}

View 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;
}

View 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;
}

View 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));
}

View 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}`;
}
}

View 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
View 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);
}
}