diff --git a/Cargo.lock b/Cargo.lock index fab47d9761..a8602aa676 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3084,9 +3084,9 @@ dependencies = [ [[package]] name = "iri-string" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" dependencies = [ "memchr", "serde", @@ -4679,9 +4679,9 @@ dependencies = [ [[package]] name = "pulldown-cmark" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c41efbf8f90ac44de7f3a868f0867851d261b56291732d0cbf7cceaaeb55a6" +checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" dependencies = [ "bitflags 2.11.0", "getopts", @@ -4801,9 +4801,9 @@ dependencies = [ [[package]] name = "quoted_printable" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73" +checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972" [[package]] name = "r-efi" @@ -5271,9 +5271,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -5507,9 +5507,9 @@ dependencies = [ [[package]] name = "serde_qs" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac22439301a0b6f45a037681518e3169e8db1db76080e2e9600a08d1027df037" +checksum = "3c742cd44662647326f86b514eadcc227fff4ce684dbbdaf1943f758d5ea058c" dependencies = [ "itoa", "percent-encoding", @@ -7949,9 +7949,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5f41c76397b7da451efd19915684f727d7e1d516384ca6bd0ec43ec94de23c" +checksum = "0b7a1c0af6e5d8d1363f4994b7a091ccf963d8b694f7da5b0b9cceb82da2c0a6" dependencies = [ "zune-core", ] diff --git a/flake.lock b/flake.lock index 1b6bbdcda0..4db212dbcd 100644 --- a/flake.lock +++ b/flake.lock @@ -32,11 +32,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1773821835, - "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", + "lastModified": 1774106199, + "narHash": "sha256-US5Tda2sKmjrg2lNHQL3jRQ6p96cgfWh3J1QBliQ8Ws=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", + "rev": "6c9a78c09ff4d6c21d0319114873508a6ec01655", "type": "github" }, "original": { @@ -74,11 +74,11 @@ ] }, "locked": { - "lastModified": 1773889863, - "narHash": "sha256-tSsmZOHBgq4qfu5MNCAEsKZL1cI4avNLw2oUTXWeb74=", + "lastModified": 1774235565, + "narHash": "sha256-D8OOwvq3zDDCtIhMcNueb9tGSZaZUanKpWDleRgQ80U=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "dbfd51be2692cb7022e301d14c139accb4ee63f0", + "rev": "dc00324a2438762582b49954373112b8eab29cab", "type": "github" }, "original": { diff --git a/web/messages/en/profile.json b/web/messages/en/profile.json index 3aba70509c..2451ba7c68 100644 --- a/web/messages/en/profile.json +++ b/web/messages/en/profile.json @@ -48,9 +48,12 @@ "profile_auth_keys_table_col_name": "Key name", "profile_auth_keys_table_menu_download_ssh": "Download SSH key", "profile_auth_keys_table_menu_download_gpg": "Download GPG key", - "profile_api_title": "API tokens", - "profile_api_add": "Add new API token", - "profile_api_empty_title": "You don't have any API tokens.", - "profile_api_empty_subtitle": "To add one, click the button below.", - "profile_api_col_name": "Token name" + "profile_api_tokens_title": "API tokens", + "profile_api_tokens_add": "Add new API token", + "profile_api_tokens_loading_title": "Checking API token availability...", + "profile_api_tokens_empty_title": "You don't have any API tokens.", + "profile_api_tokens_empty_subtitle": "To add one, click the button below.", + "profile_api_tokens_unavailable_title": "API Tokens Unavailable", + "profile_api_tokens_unavailable_subtitle": "Upgrade to the Business or Enterprise plan to enable API tokens and add one.", + "profile_api_tokens_col_name": "Token name" } diff --git a/web/src/pages/AliasesPage/tabs/AliasesDeployedTab.tsx b/web/src/pages/AliasesPage/tabs/AliasesDeployedTab.tsx index 7d787b104a..5c7d496220 100644 --- a/web/src/pages/AliasesPage/tabs/AliasesDeployedTab.tsx +++ b/web/src/pages/AliasesPage/tabs/AliasesDeployedTab.tsx @@ -1,4 +1,4 @@ -import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { useMemo, useState } from 'react'; import { m } from '../../../paraglide/messages'; @@ -8,13 +8,10 @@ import type { ButtonProps } from '../../../shared/defguard-ui/components/Button/ import { EmptyStateFlexible } from '../../../shared/defguard-ui/components/EmptyStateFlexible/EmptyStateFlexible'; import { Search } from '../../../shared/defguard-ui/components/Search/Search'; import { TableTop } from '../../../shared/defguard-ui/components/table/TableTop/TableTop'; -import { - getAliasesQueryOptions, - getLicenseInfoQueryOptions, - getRulesQueryOptions, -} from '../../../shared/query'; +import { getAliasesQueryOptions, getRulesQueryOptions } from '../../../shared/query'; import { canUseBusinessFeature, licenseActionCheck } from '../../../shared/utils/license'; import { DeletionBlockedModal } from '../../Acl/components/DeletionBlockedModal/DeletionBlockedModal'; +import { useRuleDeps } from '../../RulesPage/useRuleDeps'; import { AliasTable } from '../AliasTable'; export const AliasesDeployedTab = () => { @@ -25,9 +22,7 @@ export const AliasesDeployedTab = () => { const isEmpty = aliases.length === 0; const navigate = useNavigate(); const [search, setSearch] = useState(''); - const { data: licenseInfo, isFetching: licenseFetching } = useQuery( - getLicenseInfoQueryOptions, - ); + const { license, loading } = useRuleDeps(); const { data: rules } = useSuspenseQuery(getRulesQueryOptions); const rulesByAliasId = useMemo(() => { const map: Record = {}; @@ -51,15 +46,15 @@ export const AliasesDeployedTab = () => { iconLeft: 'add-alias', variant: 'primary', testId: 'add-alias', - disabled: licenseFetching, + disabled: loading, onClick: () => { - if (licenseInfo === undefined) return; - licenseActionCheck(canUseBusinessFeature(licenseInfo), () => { + if (license === undefined) return; + licenseActionCheck(canUseBusinessFeature(license), () => { navigate({ to: '/acl/add-alias' }); }); }, }), - [navigate, licenseFetching, licenseInfo], + [navigate, loading, license], ); const filteredAliases = useMemo(() => { diff --git a/web/src/pages/AliasesPage/tabs/AliasesPendingTab.tsx b/web/src/pages/AliasesPage/tabs/AliasesPendingTab.tsx index 7465930ecc..93f10d2068 100644 --- a/web/src/pages/AliasesPage/tabs/AliasesPendingTab.tsx +++ b/web/src/pages/AliasesPage/tabs/AliasesPendingTab.tsx @@ -5,6 +5,8 @@ import { Button } from '../../../shared/defguard-ui/components/Button/Button'; import { EmptyStateFlexible } from '../../../shared/defguard-ui/components/EmptyStateFlexible/EmptyStateFlexible'; import { TableTop } from '../../../shared/defguard-ui/components/table/TableTop/TableTop'; import { getAliasesQueryOptions, getRulesQueryOptions } from '../../../shared/query'; +import { canUseBusinessFeature, licenseActionCheck } from '../../../shared/utils/license'; +import { useRuleDeps } from '../../RulesPage/useRuleDeps'; import { AliasTable } from '../AliasTable'; export const AliasesPendingTab = () => { @@ -12,6 +14,7 @@ export const AliasesPendingTab = () => { ...getAliasesQueryOptions, select: (resp) => resp.data.filter((alias) => alias.state !== AclStatus.Applied), }); + const { license, loading } = useRuleDeps(); const { data: rules } = useSuspenseQuery(getRulesQueryOptions); const isEmpty = aliases.length === 0; const { mutate: applyAliases, isPending } = useMutation({ @@ -39,8 +42,12 @@ export const AliasesPendingTab = () => { iconLeft="deploy" text={`Deploy all pending (${aliases.length})`} loading={isPending} + disabled={loading} onClick={() => { - applyAliases(aliases.map((alias) => alias.id)); + if (license === undefined) return; + licenseActionCheck(canUseBusinessFeature(license), () => { + applyAliases(aliases.map((alias) => alias.id)); + }); }} /> )} diff --git a/web/src/pages/DestinationsPage/components/DestinationsTable.tsx b/web/src/pages/DestinationsPage/components/DestinationsTable.tsx index 91579c5003..0e335b5409 100644 --- a/web/src/pages/DestinationsPage/components/DestinationsTable.tsx +++ b/web/src/pages/DestinationsPage/components/DestinationsTable.tsx @@ -1,4 +1,4 @@ -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { createColumnHelper, @@ -7,6 +7,7 @@ import { } from '@tanstack/react-table'; import { useMemo, useState } from 'react'; import { m } from '../../../paraglide/messages'; +import api from '../../../shared/api/api'; import { type AclDestination, AclProtocolName, @@ -71,6 +72,12 @@ export const DestinationsTable = ({ getLicenseInfoQueryOptions, ); + const { mutate: applyDestinations } = useMutation({ + mutationFn: api.acl.destination.applyDestinations, + meta: { + invalidate: ['acl'], + }, + }); const columns = useMemo( () => [ columnHelper.accessor('name', { @@ -207,6 +214,18 @@ export const DestinationsTable = ({ ], }, ]; + if (row.state === 'Modified') { + menuItems[0].items.splice(1, 0, { + text: 'Deploy', + icon: 'deploy', + onClick: () => { + if (licenseInfo === undefined) return; + licenseActionCheck(canUseBusinessFeature(licenseInfo), () => { + applyDestinations([row.id]); + }); + }, + }); + } return ; }, }), @@ -218,6 +237,7 @@ export const DestinationsTable = ({ licenseFetching, licenseInfo, disableBlockedModal, + applyDestinations, ], ); diff --git a/web/src/pages/DestinationsPage/tabs/DestinationDeployedTab/DestinationDeployedTab.tsx b/web/src/pages/DestinationsPage/tabs/DestinationDeployedTab/DestinationDeployedTab.tsx index 7280f98295..b25a644d78 100644 --- a/web/src/pages/DestinationsPage/tabs/DestinationDeployedTab/DestinationDeployedTab.tsx +++ b/web/src/pages/DestinationsPage/tabs/DestinationDeployedTab/DestinationDeployedTab.tsx @@ -1,4 +1,4 @@ -import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { useMemo } from 'react'; import { AclStatus } from '../../../../shared/api/types'; @@ -6,7 +6,6 @@ import type { ButtonProps } from '../../../../shared/defguard-ui/components/Butt import { EmptyStateFlexible } from '../../../../shared/defguard-ui/components/EmptyStateFlexible/EmptyStateFlexible'; import { getDestinationsQueryOptions, - getLicenseInfoQueryOptions, getRulesQueryOptions, } from '../../../../shared/query'; import { @@ -14,6 +13,7 @@ import { licenseActionCheck, } from '../../../../shared/utils/license'; import { DeletionBlockedModal } from '../../../Acl/components/DeletionBlockedModal/DeletionBlockedModal'; +import { useRuleDeps } from '../../../RulesPage/useRuleDeps'; import { DestinationsTable } from '../../components/DestinationsTable'; export const DestinationDeployedTab = () => { @@ -24,25 +24,25 @@ export const DestinationDeployedTab = () => { }); const navigate = useNavigate(); - const { data: licenseInfo, isFetching } = useQuery(getLicenseInfoQueryOptions); const { data: rules } = useSuspenseQuery(getRulesQueryOptions); + const { license, loading } = useRuleDeps(); const addButtonProps = useMemo( (): ButtonProps => ({ text: 'Add new destination', variant: 'primary', iconLeft: 'add-location', - disabled: isFetching, + disabled: loading, onClick: () => { - if (licenseInfo === undefined) return; - licenseActionCheck(canUseBusinessFeature(licenseInfo), () => { + if (license === undefined) return; + licenseActionCheck(canUseBusinessFeature(license), () => { navigate({ to: '/acl/add-destination', }); }); }, }), - [navigate, isFetching, licenseInfo], + [navigate, loading, license], ); return ( diff --git a/web/src/pages/DestinationsPage/tabs/DestinationPendingTab/DestinationPendingTab.tsx b/web/src/pages/DestinationsPage/tabs/DestinationPendingTab/DestinationPendingTab.tsx index 5aff03f7ce..902e2dbe26 100644 --- a/web/src/pages/DestinationsPage/tabs/DestinationPendingTab/DestinationPendingTab.tsx +++ b/web/src/pages/DestinationsPage/tabs/DestinationPendingTab/DestinationPendingTab.tsx @@ -8,6 +8,11 @@ import { getDestinationsQueryOptions, getRulesQueryOptions, } from '../../../../shared/query'; +import { + canUseBusinessFeature, + licenseActionCheck, +} from '../../../../shared/utils/license'; +import { useRuleDeps } from '../../../RulesPage/useRuleDeps'; import { DestinationsTable } from '../../components/DestinationsTable'; export const DestinationPendingTab = () => { @@ -17,6 +22,7 @@ export const DestinationPendingTab = () => { resp.data.filter((destination) => destination.state !== AclStatus.Applied), }); const { data: rules } = useSuspenseQuery(getRulesQueryOptions); + const { license, loading } = useRuleDeps(); const { mutate, isPending } = useMutation({ mutationFn: api.acl.destination.applyDestinations, @@ -30,11 +36,15 @@ export const DestinationPendingTab = () => { text: `Deploy all pending (${destinations.length})`, iconLeft: 'deploy', loading: isPending, + disabled: loading, onClick: () => { - mutate(destinations.map((destination) => destination.id)); + if (license === undefined) return; + licenseActionCheck(canUseBusinessFeature(license), () => { + mutate(destinations.map((destination) => destination.id)); + }); }, }), - [isPending, mutate, destinations], + [isPending, mutate, destinations, license, loading], ); return ( diff --git a/web/src/pages/RulesPage/RulesTable.tsx b/web/src/pages/RulesPage/RulesTable.tsx index fb4af4a27d..2a17257999 100644 --- a/web/src/pages/RulesPage/RulesTable.tsx +++ b/web/src/pages/RulesPage/RulesTable.tsx @@ -352,7 +352,9 @@ export const RulesTable = ({ icon: 'disabled', text: m.controls_disable(), onClick: () => { - toggleRule(row.id); + licenseActionCheck(canUseBusinessFeature(license), () => { + toggleRule(row.id); + }); }, }); } else { @@ -360,7 +362,9 @@ export const RulesTable = ({ icon: 'check', text: m.controls_enable(), onClick: () => { - toggleRule(row.id); + licenseActionCheck(canUseBusinessFeature(license), () => { + toggleRule(row.id); + }); }, }); } @@ -369,8 +373,11 @@ export const RulesTable = ({ topItems.push({ icon: 'deploy', text: m.controls_deploy(), + onClick: () => { - deployRule([row.id]); + licenseActionCheck(canUseBusinessFeature(license), () => { + deployRule([row.id]); + }); }, }); break; diff --git a/web/src/pages/RulesPage/tabs/RulesDeployedTab.tsx b/web/src/pages/RulesPage/tabs/RulesDeployedTab.tsx index 96305ca4c7..0990d80f51 100644 --- a/web/src/pages/RulesPage/tabs/RulesDeployedTab.tsx +++ b/web/src/pages/RulesPage/tabs/RulesDeployedTab.tsx @@ -1,4 +1,4 @@ -import { useQuery, useSuspenseQuery } from '@tanstack/react-query'; +import { useSuspenseQuery } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; import { useMemo } from 'react'; import { AclStatus } from '../../../shared/api/types'; @@ -6,7 +6,7 @@ import { TableSkeleton } from '../../../shared/components/skeleton/TableSkeleton import type { ButtonProps } from '../../../shared/defguard-ui/components/Button/types'; import { EmptyStateFlexible } from '../../../shared/defguard-ui/components/EmptyStateFlexible/EmptyStateFlexible'; import { isPresent } from '../../../shared/defguard-ui/utils/isPresent'; -import { getLicenseInfoQueryOptions, getRulesQueryOptions } from '../../../shared/query'; +import { getRulesQueryOptions } from '../../../shared/query'; import { canUseBusinessFeature, licenseActionCheck } from '../../../shared/utils/license'; import { RulesTable } from '../RulesTable'; import { useRuleDeps } from '../useRuleDeps'; @@ -21,30 +21,26 @@ export const RulesDeployedTab = () => { const navigate = useNavigate(); - const { data: licenseInfo, isFetching: licenseFetching } = useQuery( - getLicenseInfoQueryOptions, - ); + const { aliases, destinations, groups, locations, users, devices, license, loading } = + useRuleDeps(); const buttonProps = useMemo( (): ButtonProps => ({ variant: 'primary', text: 'Create new rule', iconLeft: 'add-rule', - disabled: licenseFetching, + disabled: loading, onClick: () => { - if (licenseInfo === undefined) return; + if (license === undefined) return; - licenseActionCheck(canUseBusinessFeature(licenseInfo), () => { + licenseActionCheck(canUseBusinessFeature(license), () => { navigate({ to: '/acl/add-rule' }); }); }, }), - [navigate, licenseFetching, licenseInfo], + [navigate, loading, license], ); - const { aliases, destinations, groups, locations, users, devices, license, loading } = - useRuleDeps(); - return ( <> {isEmpty && ( diff --git a/web/src/pages/user-profile/UserProfilePage/UserProfilePage.tsx b/web/src/pages/user-profile/UserProfilePage/UserProfilePage.tsx index 2d1a0a0469..3bacf32a7f 100644 --- a/web/src/pages/user-profile/UserProfilePage/UserProfilePage.tsx +++ b/web/src/pages/user-profile/UserProfilePage/UserProfilePage.tsx @@ -9,16 +9,23 @@ import { Tabs } from '../../../shared/defguard-ui/components/Tabs/Tabs'; import type { TabsItem } from '../../../shared/defguard-ui/components/Tabs/types'; import { useAuth } from '../../../shared/hooks/useAuth'; import { + getLicenseInfoQueryOptions, getUserApiTokensQueryOptions, getUserAuthKeysQueryOptions, userProfileQueryOptions, } from '../../../shared/query'; +import { canUseBusinessFeature } from '../../../shared/utils/license'; import { createUserProfileStore, UserProfileContext } from './hooks/useUserProfilePage'; import { ProfileApiTokensTab } from './tabs/ProfileApiTokensTab/ProfileApiTokensTab'; import { ProfileAuthKeysTab } from './tabs/ProfileAuthKeysTab/ProfileAuthKeysTab'; import { ProfileDetailsTab } from './tabs/ProfileDetailsTab/ProfileDetailsTab'; import { ProfileDevicesTab } from './tabs/ProfileDevicesTab/ProfileDevicesTab'; -import { UserProfileTab, type UserProfileTabValue } from './tabs/types'; +import { + ApiTokensTabAvailability, + type ApiTokensTabAvailabilityValue, + UserProfileTab, + type UserProfileTabValue, +} from './tabs/types'; const defaultTab = UserProfileTab.Details; @@ -40,8 +47,25 @@ export const UserProfilePage = () => { const { data: userProfile } = useSuspenseQuery(userProfileQueryOptions(username)); const { data: userAuthKeys } = useSuspenseQuery(getUserAuthKeysQueryOptions(username)); - const { data: userApiTokens } = useQuery( - getUserApiTokensQueryOptions(username, isAdmin), + const { data: licenseInfo } = useQuery(getLicenseInfoQueryOptions); + const apiTokensTabAvailability = useMemo((): ApiTokensTabAvailabilityValue => { + if (!isAdmin) { + return ApiTokensTabAvailability.Hidden; + } + + if (licenseInfo === undefined) { + return ApiTokensTabAvailability.Loading; + } + + return canUseBusinessFeature(licenseInfo).result + ? ApiTokensTabAvailability.Available + : ApiTokensTabAvailability.Unavailable; + }, [isAdmin, licenseInfo]); + const { data: userApiTokens, isPending: userApiTokensPending } = useQuery( + getUserApiTokensQueryOptions( + username, + apiTokensTabAvailability === ApiTokensTabAvailability.Available, + ), ); const pageTitle = useMemo(() => { @@ -75,6 +99,14 @@ export const UserProfilePage = () => { [navigate], ); + const setApiTokensTabIfAllowed = useCallback(() => { + if (apiTokensTabAvailability === ApiTokensTabAvailability.Hidden) { + return; + } + + setActiveTab(UserProfileTab.ApiTokens); + }, [apiTokensTabAvailability, setActiveTab]); + const tabsConfiguration = useMemo(() => { const res: TabsItem[] = [ { @@ -95,36 +127,51 @@ export const UserProfilePage = () => { { title: m.profile_tabs_api(), active: activeTab === UserProfileTab.ApiTokens, - hidden: !isAdmin, - onClick: () => setActiveTab(UserProfileTab.ApiTokens), + hidden: apiTokensTabAvailability === ApiTokensTabAvailability.Hidden, + onClick: setApiTokensTabIfAllowed, }, ]; return res; - }, [activeTab, setActiveTab, isAdmin]); + }, [apiTokensTabAvailability, setActiveTab, setApiTokensTabIfAllowed, activeTab]); - const RenderActiveTab = useMemo(() => { + const activeTabContent = useMemo(() => { switch (activeTab) { case UserProfileTab.Details: - return ProfileDetailsTab; + return ; case UserProfileTab.Devices: - return ProfileDevicesTab; + return ; case UserProfileTab.AuthKeys: - return ProfileAuthKeysTab; + return ; case UserProfileTab.ApiTokens: - return ProfileApiTokensTab; + return ( + + ); + default: + return ; } - }, [activeTab]); + }, [activeTab, apiTokensTabAvailability, userApiTokensPending]); useEffect(() => { - if (activeTab === 'api-tokens' && !isAdmin) { - navigate({ - from: '/user/$username', - search: { - tab: 'details', - }, - }); + if ( + activeTab !== UserProfileTab.ApiTokens || + apiTokensTabAvailability !== ApiTokensTabAvailability.Hidden + ) { + return; } - }, [activeTab, isAdmin, navigate]); + + navigate({ + from: '/user/$username', + search: { + tab: UserProfileTab.Details, + }, + }); + }, [activeTab, apiTokensTabAvailability, navigate]); // biome-ignore lint/correctness/useExhaustiveDependencies: side effect useEffect(() => { @@ -157,7 +204,7 @@ export const UserProfilePage = () => { - + {activeTabContent} ); diff --git a/web/src/pages/user-profile/UserProfilePage/tabs/ProfileApiTokensTab/ProfileApiTokensTab.tsx b/web/src/pages/user-profile/UserProfilePage/tabs/ProfileApiTokensTab/ProfileApiTokensTab.tsx index e8d64ae8b4..67aae645da 100644 --- a/web/src/pages/user-profile/UserProfilePage/tabs/ProfileApiTokensTab/ProfileApiTokensTab.tsx +++ b/web/src/pages/user-profile/UserProfilePage/tabs/ProfileApiTokensTab/ProfileApiTokensTab.tsx @@ -8,24 +8,55 @@ import { openModal } from '../../../../../shared/hooks/modalControls/modalsSubje import { ModalName } from '../../../../../shared/hooks/modalControls/modalTypes'; import { ProfileTabHeader } from '../../components/ProfileTabHeader/ProfileTabHeader'; import { useUserProfile } from '../../hooks/useUserProfilePage'; +import { ApiTokensTabAvailability, type ApiTokensTabAvailabilityValue } from '../types'; import { ProfileApiTokensTable } from './components/ProfileApiTokensTable/ProfileApiTokensTable'; import { AddApiTokenModal } from './modals/AddApiTokenModal/AddApiTokenModal'; import { RenameApiTokenModal } from './modals/RenameApiTokenModal/RenameApiTokenModal'; -export const ProfileApiTokensTab = () => { +type Props = { + availability: ApiTokensTabAvailabilityValue; + isLoading: boolean; +}; + +export const ProfileApiTokensTab = ({ availability, isLoading }: Props) => { + if (availability === ApiTokensTabAvailability.Hidden) { + return null; + } + + if (availability === ApiTokensTabAvailability.Loading || isLoading) { + return ( + + ); + } + + if (availability === ApiTokensTabAvailability.Unavailable) { + return ( + + ); + } + + return ; +}; + +const AvailableProfileApiTokensTab = () => { const username = useUserProfile((s) => s.user.username); const apiTokens = useUserProfile((s) => s.apiTokens); + return ( <> {apiTokens.length === 0 && ( { openModal(ModalName.AddApiToken, { username, @@ -37,9 +68,9 @@ export const ProfileApiTokensTab = () => { {apiTokens.length > 0 && ( - +