From: guillaume-peoch-sonarsource Date: Thu, 30 Nov 2023 11:09:06 +0000 (+0100) Subject: SONAR-21119 UI for GitLab Authentication tab with users and groups provisioning X-Git-Tag: 10.4.0.87286~274 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=e4e087dd7877352e2d77f9531535258224820e9f;p=sonarqube.git SONAR-21119 UI for GitLab Authentication tab with users and groups provisioning --- diff --git a/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts index c4664fc7ddc..d382f32c511 100644 --- a/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/AuthenticationServiceMock.ts @@ -17,12 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { cloneDeep } from 'lodash'; +import { cloneDeep, omit } from 'lodash'; +import { mockGitlabConfiguration } from '../../helpers/mocks/alm-integrations'; import { mockTask } from '../../helpers/mocks/tasks'; +import { mockPaging } from '../../helpers/testMocks'; import { GitHubConfigurationStatus, GitHubMapping, GitHubProvisioningStatus, + GitlabConfiguration, } from '../../types/provisioning'; import { Task, TaskStatuses, TaskTypes } from '../../types/tasks'; import { @@ -30,12 +33,17 @@ import { activateScim, addGithubRolesMapping, checkConfigurationValidity, + createGitLabConfiguration, deactivateGithubProvisioning, deactivateScim, + deleteGitLabConfiguration, deleteGithubRolesMapping, + fetchGitLabConfiguration, + fetchGitLabConfigurations, fetchGithubProvisioningStatus, fetchGithubRolesMapping, fetchIsScimEnabled, + updateGitLabConfiguration, updateGithubRolesMapping, } from '../provisioning'; @@ -63,6 +71,10 @@ const defaultConfigurationStatus: GitHubConfigurationStatus = { ], }; +const defaultGitlabConfiguration: GitlabConfiguration[] = [ + mockGitlabConfiguration({ id: '1', enabled: true }), +]; + const githubMappingMock = ( id: string, permissions: (keyof GitHubMapping['permissions'])[], @@ -107,6 +119,7 @@ export default class AuthenticationServiceMock { githubConfigurationStatus: GitHubConfigurationStatus; githubMapping: GitHubMapping[]; tasks: Task[]; + gitlabConfigurations: GitlabConfiguration[]; constructor() { this.scimStatus = false; @@ -114,6 +127,7 @@ export default class AuthenticationServiceMock { this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus); this.githubMapping = cloneDeep(defaultMapping); this.tasks = []; + this.gitlabConfigurations = cloneDeep(defaultGitlabConfiguration); jest.mocked(activateScim).mockImplementation(this.handleActivateScim); jest.mocked(deactivateScim).mockImplementation(this.handleDeactivateScim); jest.mocked(fetchIsScimEnabled).mockImplementation(this.handleFetchIsScimEnabled); @@ -133,6 +147,11 @@ export default class AuthenticationServiceMock { jest.mocked(updateGithubRolesMapping).mockImplementation(this.handleUpdateGithubRolesMapping); jest.mocked(addGithubRolesMapping).mockImplementation(this.handleAddGithubRolesMapping); jest.mocked(deleteGithubRolesMapping).mockImplementation(this.handleDeleteGithubRolesMapping); + jest.mocked(fetchGitLabConfigurations).mockImplementation(this.handleFetchGitLabConfigurations); + jest.mocked(fetchGitLabConfiguration).mockImplementation(this.handleFetchGitLabConfiguration); + jest.mocked(createGitLabConfiguration).mockImplementation(this.handleCreateGitLabConfiguration); + jest.mocked(updateGitLabConfiguration).mockImplementation(this.handleUpdateGitLabConfiguration); + jest.mocked(deleteGitLabConfiguration).mockImplementation(this.handleDeleteGitLabConfiguration); } addProvisioningTask = (overrides: Partial> = {}) => { @@ -244,11 +263,52 @@ export default class AuthenticationServiceMock { this.githubMapping = [...this.githubMapping, githubMappingMock(id, permissions)]; }; + handleFetchGitLabConfigurations: typeof fetchGitLabConfigurations = () => { + return Promise.resolve({ + configurations: this.gitlabConfigurations, + page: mockPaging({ total: this.gitlabConfigurations.length }), + }); + }; + + handleFetchGitLabConfiguration: typeof fetchGitLabConfiguration = (id: string) => { + const configuration = this.gitlabConfigurations.find((c) => c.id === id); + if (!configuration) { + return Promise.reject(); + } + return Promise.resolve(configuration); + }; + + handleCreateGitLabConfiguration: typeof createGitLabConfiguration = (data) => { + const newConfig = mockGitlabConfiguration({ + ...omit(data, 'applicationId', 'clientSecret'), + id: '1', + enabled: true, + }); + this.gitlabConfigurations = [...this.gitlabConfigurations, newConfig]; + return Promise.resolve(newConfig); + }; + + handleUpdateGitLabConfiguration: typeof updateGitLabConfiguration = (id, data) => { + const index = this.gitlabConfigurations.findIndex((c) => c.id === id); + this.gitlabConfigurations[index] = { ...this.gitlabConfigurations[index], ...data }; + return Promise.resolve(this.gitlabConfigurations[index]); + }; + + handleDeleteGitLabConfiguration: typeof deleteGitLabConfiguration = (id) => { + this.gitlabConfigurations = this.gitlabConfigurations.filter((c) => c.id !== id); + return Promise.resolve(); + }; + + setGitlabConfigurations = (gitlabConfigurations: GitlabConfiguration[]) => { + this.gitlabConfigurations = gitlabConfigurations; + }; + reset = () => { this.scimStatus = false; this.githubProvisioningStatus = false; this.githubConfigurationStatus = cloneDeep(defaultConfigurationStatus); this.githubMapping = cloneDeep(defaultMapping); this.tasks = []; + this.gitlabConfigurations = cloneDeep(defaultGitlabConfiguration); }; } diff --git a/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts index fbd14e24eb6..46a2d5e1063 100644 --- a/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/GroupsServiceMock.ts @@ -19,14 +19,13 @@ */ import { cloneDeep } from 'lodash'; -import { Provider } from '../../components/hooks/useManageProvider'; import { mockGroup, mockIdentityProvider, mockPaging, mockUserGroupMember, } from '../../helpers/testMocks'; -import { Group, IdentityProvider, Paging } from '../../types/types'; +import { Group, IdentityProvider, Paging, Provider } from '../../types/types'; import { createGroup, deleteGroup, getUsersGroups, updateGroup } from '../user_groups'; jest.mock('../user_groups'); diff --git a/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts b/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts index ca31714c1dc..6c9b3c67324 100644 --- a/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts +++ b/server/sonar-web/src/main/js/api/mocks/SystemServiceMock.ts @@ -18,10 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { cloneDeep } from 'lodash'; -import { SysInfoCluster, SysInfoLogging, SysInfoStandalone } from '../../types/types'; +import { Provider, SysInfoCluster, SysInfoLogging, SysInfoStandalone } from '../../types/types'; import { LogsLevels } from '../../apps/system/utils'; -import { Provider } from '../../components/hooks/useManageProvider'; import { mockClusterSysInfo, mockLogs, mockStandaloneSysInfo } from '../../helpers/testMocks'; import { getSystemInfo, setLogLevel } from '../system'; diff --git a/server/sonar-web/src/main/js/api/provisioning.ts b/server/sonar-web/src/main/js/api/provisioning.ts index e8c7ea90dc1..27f5a31ca01 100644 --- a/server/sonar-web/src/main/js/api/provisioning.ts +++ b/server/sonar-web/src/main/js/api/provisioning.ts @@ -18,9 +18,20 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import axios from 'axios'; +import { keyBy } from 'lodash'; import { throwGlobalError } from '../helpers/error'; import { getJSON, post, postJSON } from '../helpers/request'; -import { GitHubConfigurationStatus, GitHubMapping, GithubStatus } from '../types/provisioning'; +import { + GitHubConfigurationStatus, + GitHubMapping, + GitLabConfigurationCreateBody, + GitLabConfigurationUpdateBody, + GithubStatus, + GitlabConfiguration, + ProvisioningType, +} from '../types/provisioning'; +import { Paging } from '../types/types'; +import { getValues, resetSettingValue, setSimpleSettingValue } from './settings'; const GITHUB_PERMISSION_MAPPINGS = '/api/v2/dop-translation/github-permission-mappings'; @@ -81,3 +92,125 @@ export function addGithubRolesMapping(data: Omit) { export function deleteGithubRolesMapping(role: string) { return axios.delete(`${GITHUB_PERMISSION_MAPPINGS}/${encodeURIComponent(role)}`); } + +const GITLAB_SETTING_ENABLED = 'sonar.auth.gitlab.enabled'; +const GITLAB_SETTING_URL = 'sonar.auth.gitlab.url'; +const GITLAB_SETTING_APP_ID = 'sonar.auth.gitlab.applicationId.secured'; +const GITLAB_SETTING_SECRET = 'sonar.auth.gitlab.secret.secured'; +export const GITLAB_SETTING_ALLOW_SIGNUP = 'sonar.auth.gitlab.allowUsersToSignUp'; +const GITLAB_SETTING_GROUPS_SYNC = 'sonar.auth.gitlab.groupsSync'; +const GITLAB_SETTING_PROVISIONING_ENABLED = 'provisioning.gitlab.enabled'; +export const GITLAB_SETTING_GROUP_TOKEN = 'provisioning.gitlab.token.secured'; +export const GITLAB_SETTING_GROUPS = 'provisioning.gitlab.groups'; + +const gitlabKeys = [ + GITLAB_SETTING_ENABLED, + GITLAB_SETTING_URL, + GITLAB_SETTING_APP_ID, + GITLAB_SETTING_SECRET, + GITLAB_SETTING_ALLOW_SIGNUP, + GITLAB_SETTING_GROUPS_SYNC, + GITLAB_SETTING_PROVISIONING_ENABLED, + GITLAB_SETTING_GROUP_TOKEN, + GITLAB_SETTING_GROUPS, +]; + +const fieldKeyMap = { + enabled: GITLAB_SETTING_ENABLED, + url: GITLAB_SETTING_URL, + applicationId: GITLAB_SETTING_APP_ID, + clientSecret: GITLAB_SETTING_SECRET, + allowUsersToSignUp: GITLAB_SETTING_ALLOW_SIGNUP, + synchronizeUserGroups: GITLAB_SETTING_GROUPS_SYNC, + type: GITLAB_SETTING_PROVISIONING_ENABLED, + provisioningToken: GITLAB_SETTING_GROUP_TOKEN, + groups: GITLAB_SETTING_GROUPS, +}; + +const getGitLabConfiguration = async (): Promise => { + const values = await getValues({ + keys: gitlabKeys, + }); + const valuesMap = keyBy(values, 'key'); + if (!valuesMap[GITLAB_SETTING_APP_ID] || !valuesMap[GITLAB_SETTING_SECRET]) { + return null; + } + return { + id: '1', + enabled: valuesMap[GITLAB_SETTING_ENABLED]?.value === 'true', + url: valuesMap[GITLAB_SETTING_URL]?.value ?? 'https://gitlab.com', + synchronizeUserGroups: valuesMap[GITLAB_SETTING_GROUPS_SYNC]?.value === 'true', + type: + valuesMap[GITLAB_SETTING_PROVISIONING_ENABLED]?.value === 'true' + ? ProvisioningType.auto + : ProvisioningType.jit, + groups: valuesMap[GITLAB_SETTING_GROUPS]?.values + ? valuesMap[GITLAB_SETTING_GROUPS]?.values + : [], + allowUsersToSignUp: valuesMap[GITLAB_SETTING_ALLOW_SIGNUP]?.value === 'true', + }; +}; + +export async function fetchGitLabConfigurations(): Promise<{ + configurations: GitlabConfiguration[]; + page: Paging; +}> { + const config = await getGitLabConfiguration(); + return { + configurations: config ? [config] : [], + page: { + pageIndex: 1, + pageSize: 1, + total: config ? 1 : 0, + }, + }; +} + +export async function fetchGitLabConfiguration(_id: string): Promise { + const configuration = await getGitLabConfiguration(); + if (!configuration) { + return Promise.reject(new Error('GitLab configuration not found')); + } + return Promise.resolve(configuration); +} + +export async function createGitLabConfiguration( + configuration: GitLabConfigurationCreateBody, +): Promise { + await Promise.all( + Object.entries(configuration).map( + ([key, value]: [key: keyof GitLabConfigurationCreateBody, value: string]) => + setSimpleSettingValue({ key: fieldKeyMap[key], value }), + ), + ); + await setSimpleSettingValue({ key: fieldKeyMap.enabled, value: 'true' }); + return fetchGitLabConfiguration(''); +} + +export async function updateGitLabConfiguration( + _id: string, + configuration: Partial, +): Promise { + await Promise.all( + Object.entries(configuration).map( + ([key, value]: [key: keyof typeof fieldKeyMap, value: string | string[]]) => { + if (fieldKeyMap[key] === GITLAB_SETTING_PROVISIONING_ENABLED) { + return setSimpleSettingValue({ + key: fieldKeyMap[key], + value: value === ProvisioningType.auto ? 'true' : 'false', + }); + } else if (typeof value === 'boolean') { + return setSimpleSettingValue({ key: fieldKeyMap[key], value: value ? 'true' : 'false' }); + } else if (Array.isArray(value)) { + return setSimpleSettingValue({ key: fieldKeyMap[key], values: value }); + } + return setSimpleSettingValue({ key: fieldKeyMap[key], value }); + }, + ), + ); + return fetchGitLabConfiguration(''); +} + +export function deleteGitLabConfiguration(_id: string): Promise { + return resetSettingValue({ keys: gitlabKeys.join(',') }); +} diff --git a/server/sonar-web/src/main/js/api/settings.ts b/server/sonar-web/src/main/js/api/settings.ts index 2c2f5111526..53580197b9b 100644 --- a/server/sonar-web/src/main/js/api/settings.ts +++ b/server/sonar-web/src/main/js/api/settings.ts @@ -86,7 +86,7 @@ export function setSettingValue( } export function setSimpleSettingValue( - data: { component?: string; value: string; key: string } & BranchParameters, + data: { component?: string; value?: string; values?: string[]; key: string } & BranchParameters, ): Promise { return post('/api/settings/set', data).catch(throwGlobalError); } diff --git a/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx b/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx new file mode 100644 index 00000000000..83e933e4858 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/AlmSynchronisationWarning.tsx @@ -0,0 +1,167 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { formatDistance } from 'date-fns'; +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import Link from '../../components/common/Link'; +import CheckIcon from '../../components/icons/CheckIcon'; +import WarningIcon from '../../components/icons/WarningIcon'; +import { Alert } from '../../components/ui/Alert'; +import { translate, translateWithParameters } from '../../helpers/l10n'; +import { AlmSyncStatus } from '../../types/provisioning'; +import { TaskStatuses } from '../../types/tasks'; +import './SystemAnnouncement.css'; + +interface SynchronisationWarningProps { + short?: boolean; + data: AlmSyncStatus; +} + +interface LastSyncProps { + short?: boolean; + info: AlmSyncStatus['lastSync']; +} + +function LastSyncAlert({ info, short }: Readonly) { + if (info === undefined) { + return null; + } + const { finishedAt, errorMessage, status, summary, warningMessage } = info; + + const formattedDate = finishedAt ? formatDistance(new Date(finishedAt), new Date()) : ''; + + if (short) { + return status === TaskStatuses.Success ? ( +
+ + {warningMessage ? ( + + ) : ( + + )} + + + {warningMessage ? ( + + {translate('settings.authentication.github.synchronization_details_link')} + + ), + }} + /> + ) : ( + translateWithParameters( + 'settings.authentication.github.synchronization_successful', + formattedDate, + ) + )} + +
+ ) : ( + + + {translate('settings.authentication.github.synchronization_details_link')} + + ), + }} + /> + + ); + } + + return ( + <> + + {status === TaskStatuses.Success ? ( + <> + {translateWithParameters( + 'settings.authentication.github.synchronization_successful', + formattedDate, + )} +
+ {summary ?? ''} + + ) : ( + +
+ {translateWithParameters( + 'settings.authentication.github.synchronization_failed', + formattedDate, + )} +
+
+ {errorMessage ?? ''} +
+ )} +
+ + {warningMessage} + + + ); +} + +export default function AlmSynchronisationWarning({ + short, + data, +}: Readonly) { + return ( + <> + + {!short && + data?.nextSync && + translate( + data.nextSync.status === TaskStatuses.Pending + ? 'settings.authentication.github.synchronization_pending' + : 'settings.authentication.github.synchronization_in_progress', + )} + + + + + ); +} diff --git a/server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx b/server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx index 09860d1d703..22dcf68356e 100644 --- a/server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx +++ b/server/sonar-web/src/main/js/app/components/GitHubSynchronisationWarning.tsx @@ -17,156 +17,23 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { formatDistance } from 'date-fns'; import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import Link from '../../components/common/Link'; -import CheckIcon from '../../components/icons/CheckIcon'; -import WarningIcon from '../../components/icons/WarningIcon'; -import { Alert } from '../../components/ui/Alert'; -import { translate, translateWithParameters } from '../../helpers/l10n'; import { useGitHubSyncStatusQuery } from '../../queries/identity-provider'; -import { GithubStatusEnabled } from '../../types/provisioning'; -import { TaskStatuses } from '../../types/tasks'; +import AlmSynchronisationWarning from './AlmSynchronisationWarning'; import './SystemAnnouncement.css'; -interface LastSyncProps { +interface Props { short?: boolean; - info: GithubStatusEnabled['lastSync']; } -interface GitHubSynchronisationWarningProps { - short?: boolean; -} - -function LastSyncAlert({ info, short }: LastSyncProps) { - if (info === undefined) { - return null; - } - const { finishedAt, errorMessage, status, summary, warningMessage } = info; - - const formattedDate = finishedAt ? formatDistance(new Date(finishedAt), new Date()) : ''; - - if (short) { - return status === TaskStatuses.Success ? ( -
- - {warningMessage ? ( - - ) : ( - - )} - - - {warningMessage ? ( - - {translate('settings.authentication.github.synchronization_details_link')} - - ), - }} - /> - ) : ( - translateWithParameters( - 'settings.authentication.github.synchronization_successful', - formattedDate, - ) - )} - -
- ) : ( - - - {translate('settings.authentication.github.synchronization_details_link')} - - ), - }} - /> - - ); - } - - return ( - <> - - {status === TaskStatuses.Success ? ( - <> - {translateWithParameters( - 'settings.authentication.github.synchronization_successful', - formattedDate, - )} -
- {summary ?? ''} - - ) : ( - -
- {translateWithParameters( - 'settings.authentication.github.synchronization_failed', - formattedDate, - )} -
-
- {errorMessage ?? ''} -
- )} -
- - {warningMessage} - - - ); -} - -function GitHubSynchronisationWarning({ short }: GitHubSynchronisationWarningProps) { +function GitHubSynchronisationWarning({ short }: Readonly) { const { data } = useGitHubSyncStatusQuery(); if (!data) { return null; } - return ( - <> - - {!short && - data?.nextSync && - translate( - data.nextSync.status === TaskStatuses.Pending - ? 'settings.authentication.github.synchronization_pending' - : 'settings.authentication.github.synchronization_in_progress', - )} - - - - - ); + return ; } export default GitHubSynchronisationWarning; diff --git a/server/sonar-web/src/main/js/app/components/GitLabSynchronisationWarning.tsx b/server/sonar-web/src/main/js/app/components/GitLabSynchronisationWarning.tsx new file mode 100644 index 00000000000..227e2e55129 --- /dev/null +++ b/server/sonar-web/src/main/js/app/components/GitLabSynchronisationWarning.tsx @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import * as React from 'react'; +import { useGitLabSyncStatusQuery } from '../../queries/identity-provider'; +import AlmSynchronisationWarning from './AlmSynchronisationWarning'; +import './SystemAnnouncement.css'; + +interface Props { + short?: boolean; +} + +function GitLabSynchronisationWarning({ short }: Readonly) { + const { data } = useGitLabSyncStatusQuery(); + + if (!data) { + return null; + } + + return ; +} + +export default GitLabSynchronisationWarning; diff --git a/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx b/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx index 6aef407edc8..4927e76985c 100644 --- a/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx +++ b/server/sonar-web/src/main/js/apps/groups/GroupsApp.tsx @@ -25,9 +25,10 @@ import ListFooter from '../../components/controls/ListFooter'; import { ManagedFilter } from '../../components/controls/ManagedFilter'; import SearchBox from '../../components/controls/SearchBox'; import Suggestions from '../../components/embed-docs-modal/Suggestions'; -import { Provider, useManageProvider } from '../../components/hooks/useManageProvider'; import { translate } from '../../helpers/l10n'; import { useGroupsQueries } from '../../queries/groups'; +import { useIdentityProviderQuery } from '../../queries/identity-provider'; +import { Provider } from '../../types/types'; import Header from './components/Header'; import List from './components/List'; import './groups.css'; @@ -35,7 +36,7 @@ import './groups.css'; export default function GroupsApp() { const [search, setSearch] = useState(''); const [managed, setManaged] = useState(); - const manageProvider = useManageProvider(); + const { data: manageProvider } = useIdentityProviderQuery(); const { data, isLoading, fetchNextPage } = useGroupsQueries({ q: search, @@ -49,12 +50,12 @@ export default function GroupsApp() {
-
- {manageProvider === Provider.Github && } +
+ {manageProvider?.provider === Provider.Github && }
- +