diff --git a/plugins/rbac/dev/index.tsx b/plugins/rbac/dev/index.tsx index 06578af795..8c21f7e23e 100644 --- a/plugins/rbac/dev/index.tsx +++ b/plugins/rbac/dev/index.tsx @@ -34,6 +34,12 @@ class MockRBACApi implements RBACAPI { constructor(fixtureData: Role[]) { this.resources = fixtureData; } + async isLicensePluginEnabled(): Promise { + return false; + } + async downloadStatistics(): Promise { + return { status: 200 } as Response; + } async getRoles(): Promise { return this.resources; diff --git a/plugins/rbac/src/api/LicensedUsersClient.ts b/plugins/rbac/src/api/LicensedUsersClient.ts new file mode 100644 index 0000000000..8c3e40ef4e --- /dev/null +++ b/plugins/rbac/src/api/LicensedUsersClient.ts @@ -0,0 +1,61 @@ +import { + ConfigApi, + createApiRef, + IdentityApi, +} from '@backstage/core-plugin-api'; + +export type LicensedUsersAPI = { + isLicensePluginEnabled(): Promise; + downloadStatistics: () => Promise; +}; + +// @public +export const licensedUsersApiRef = createApiRef({ + id: 'plugin.licensed-users-info.service', +}); + +export type Options = { + configApi: ConfigApi; + identityApi: IdentityApi; +}; + +export class LicensedUsersAPIClient implements LicensedUsersAPI { + // @ts-ignore + private readonly configApi: ConfigApi; + private readonly identityApi: IdentityApi; + + constructor(options: Options) { + this.configApi = options.configApi; + this.identityApi = options.identityApi; + } + async isLicensePluginEnabled(): Promise { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const jsonResponse = await fetch( + `${backendUrl}/api/licensed-users-info/health`, + { + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + }, + }, + ); + + return jsonResponse.ok; + } + + async downloadStatistics(): Promise { + const { token: idToken } = await this.identityApi.getCredentials(); + const backendUrl = this.configApi.getString('backend.baseUrl'); + const response = await fetch( + `${backendUrl}/api/licensed-users-info/users`, + { + method: 'GET', + headers: { + ...(idToken && { Authorization: `Bearer ${idToken}` }), + 'Content-Type': 'text/csv', + }, + }, + ); + return response; + } +} diff --git a/plugins/rbac/src/components/DownloadUserStatistics.tsx b/plugins/rbac/src/components/DownloadUserStatistics.tsx new file mode 100644 index 0000000000..979343e238 --- /dev/null +++ b/plugins/rbac/src/components/DownloadUserStatistics.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { makeStyles } from '@material-ui/core'; + +import { licensedUsersApiRef } from '../api/LicensedUsersClient'; + +const useStyles = makeStyles(theme => ({ + linkStyle: { + color: theme.palette.link, + textDecoration: 'underline', + }, +})); + +function DownloadCSVLink() { + const classes = useStyles(); + const licensedUsersClient = useApi(licensedUsersApiRef); + const handleDownload = async ( + event: React.MouseEvent, + ) => { + event.preventDefault(); // Prevent the default link behavior + + try { + const response = await licensedUsersClient.downloadStatistics(); + + if (response.ok) { + // Get the CSV data as a string + const csvData = await response.text(); + + // Create a Blob from the CSV data + const blob = new Blob([csvData], { type: 'text/csv' }); + const url = window.URL.createObjectURL(blob); + + // Create a temporary link to trigger the download + const a = document.createElement('a'); + a.href = url; + a.download = 'licensed-users.csv'; + document.body.appendChild(a); + a.click(); + + // Clean up the temporary link and object URL + document.body.removeChild(a); + window.URL.revokeObjectURL(url); + } else { + throw new Error( + `Failed to download the csv file with list licensed users ${response.statusText}`, + ); + } + } catch (error) { + throw new Error(`Error during the download: ${error}`); + } + }; + + return ( + + Download User List + + ); +} + +export default DownloadCSVLink; diff --git a/plugins/rbac/src/components/RbacPage.test.tsx b/plugins/rbac/src/components/RbacPage.test.tsx index eb79c903c5..2db739fabe 100644 --- a/plugins/rbac/src/components/RbacPage.test.tsx +++ b/plugins/rbac/src/components/RbacPage.test.tsx @@ -8,6 +8,7 @@ import { renderInTestApp } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; +import { useCheckIfLicensePluginEnabled } from '../hooks/useCheckIfLicensePluginEnabled'; import { useRoles } from '../hooks/useRoles'; import { RbacPage } from './RbacPage'; @@ -20,12 +21,21 @@ jest.mock('../hooks/useRoles', () => ({ useRoles: jest.fn(), })); +jest.mock('../hooks/useCheckIfLicensePluginEnabled', () => ({ + useCheckIfLicensePluginEnabled: jest.fn(), +})); + const mockUsePermission = usePermission as jest.MockedFunction< typeof usePermission >; const mockUseRoles = useRoles as jest.MockedFunction; +const mockUseCheckIfLicensePluginEnabled = + useCheckIfLicensePluginEnabled as jest.MockedFunction< + typeof useCheckIfLicensePluginEnabled + >; + const RequirePermissionMock = RequirePermission as jest.MockedFunction< typeof RequirePermission >; @@ -46,6 +56,14 @@ describe('RbacPage', () => { createRoleAllowed: false, createRoleLoading: false, }); + mockUseCheckIfLicensePluginEnabled.mockReturnValue({ + loading: false, + isEnabled: false, + licenseCheckError: { + message: '', + name: '', + }, + }); await renderInTestApp(); expect(screen.getByText('Administration')).toBeInTheDocument(); }); diff --git a/plugins/rbac/src/components/RolesList/RolesList.test.tsx b/plugins/rbac/src/components/RolesList/RolesList.test.tsx index 0fd11ea3ff..e39c628a49 100644 --- a/plugins/rbac/src/components/RolesList/RolesList.test.tsx +++ b/plugins/rbac/src/components/RolesList/RolesList.test.tsx @@ -6,6 +6,7 @@ import { } from '@backstage/plugin-permission-react'; import { renderInTestApp } from '@backstage/test-utils'; +import { useCheckIfLicensePluginEnabled } from '../../hooks/useCheckIfLicensePluginEnabled'; import { useRoles } from '../../hooks/useRoles'; import { RolesData } from '../../types'; import { RolesList } from './RolesList'; @@ -19,6 +20,10 @@ jest.mock('../../hooks/useRoles', () => ({ useRoles: jest.fn(), })); +jest.mock('../../hooks/useCheckIfLicensePluginEnabled', () => ({ + useCheckIfLicensePluginEnabled: jest.fn(), +})); + const useRolesMockData: RolesData[] = [ { name: 'role:default/guests', @@ -53,6 +58,10 @@ const mockUsePermission = usePermission as jest.MockedFunction< >; const mockUseRoles = useRoles as jest.MockedFunction; +const mockUseCheckIfLicensePluginEnabled = + useCheckIfLicensePluginEnabled as jest.MockedFunction< + typeof useCheckIfLicensePluginEnabled + >; const RequirePermissionMock = RequirePermission as jest.MockedFunction< typeof RequirePermission @@ -74,6 +83,14 @@ describe('RolesList', () => { createRoleAllowed: false, createRoleLoading: false, }); + mockUseCheckIfLicensePluginEnabled.mockReturnValue({ + loading: false, + isEnabled: false, + licenseCheckError: { + message: '', + name: '', + }, + }); const { queryByText } = await renderInTestApp(); expect(queryByText('All roles (2)')).not.toBeNull(); expect(queryByText('role:default/guests')).not.toBeNull(); diff --git a/plugins/rbac/src/components/RolesList/RolesList.tsx b/plugins/rbac/src/components/RolesList/RolesList.tsx index 203a1f94dc..cdb783400e 100644 --- a/plugins/rbac/src/components/RolesList/RolesList.tsx +++ b/plugins/rbac/src/components/RolesList/RolesList.tsx @@ -1,12 +1,14 @@ import React from 'react'; -import { Table, WarningPanel } from '@backstage/core-components'; +import { Progress, Table, WarningPanel } from '@backstage/core-components'; import { makeStyles } from '@material-ui/core'; +import { useCheckIfLicensePluginEnabled } from '../../hooks/useCheckIfLicensePluginEnabled'; import { useLocationToast } from '../../hooks/useLocationToast'; import { useRoles } from '../../hooks/useRoles'; import { RolesData } from '../../types'; +import DownloadCSVLink from '../DownloadUserStatistics'; import { SnackbarAlert } from '../SnackbarAlert'; import { useToast } from '../ToastContext'; import { useDeleteDialog } from './DeleteDialogContext'; @@ -68,6 +70,11 @@ export const RolesList = () => { const errorWarning = getErrorWarning(); + const isLicensePluginEnabled = useCheckIfLicensePluginEnabled(); + if (isLicensePluginEnabled.loading) { + return ; + } + return ( <> @@ -101,6 +108,7 @@ export const RolesList = () => { } /> + {isLicensePluginEnabled.isEnabled && } {openDialog && ( { + const licensedUsersClient = useApi(licensedUsersApiRef); + const { + value: isEnabled, + loading, + error: licenseCheckError, + } = useAsync(async () => await licensedUsersClient.isLicensePluginEnabled()); + + return { + loading, + isEnabled, + licenseCheckError: licenseCheckError as Error, + }; +}; diff --git a/plugins/rbac/src/plugin.ts b/plugins/rbac/src/plugin.ts index 5efb8359be..1d2c2754a0 100644 --- a/plugins/rbac/src/plugin.ts +++ b/plugins/rbac/src/plugin.ts @@ -7,6 +7,10 @@ import { identityApiRef, } from '@backstage/core-plugin-api'; +import { + LicensedUsersAPIClient, + licensedUsersApiRef, +} from './api/LicensedUsersClient'; import { rbacApiRef, RBACBackendClient } from './api/RBACBackendClient'; import { createRoleRouteRef, roleRouteRef, rootRouteRef } from './routes'; @@ -27,6 +31,15 @@ export const rbacPlugin = createPlugin({ factory: ({ configApi, identityApi }) => new RBACBackendClient({ configApi, identityApi }), }), + createApiFactory({ + api: licensedUsersApiRef, + deps: { + configApi: configApiRef, + identityApi: identityApiRef, + }, + factory: ({ configApi, identityApi }) => + new LicensedUsersAPIClient({ configApi, identityApi }), + }), ], });