Skip to content

Commit

Permalink
feat(licensed-users): add licensed users download link (#2091)
Browse files Browse the repository at this point in the history
* feat(licensed-users): add licensed users download link

Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>

* feat(licensed-users): rename link, apply css styles to it

Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>

* feat(licensed-users): fix unit tests

Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>

---------

Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
  • Loading branch information
AndrienkoAleksandr authored Aug 28, 2024
1 parent 384434d commit b3506ff
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 1 deletion.
6 changes: 6 additions & 0 deletions plugins/rbac/dev/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ class MockRBACApi implements RBACAPI {
constructor(fixtureData: Role[]) {
this.resources = fixtureData;
}
async isLicensePluginEnabled(): Promise<boolean> {
return false;
}
async downloadStatistics(): Promise<Response> {
return { status: 200 } as Response;
}

async getRoles(): Promise<Role[]> {
return this.resources;
Expand Down
61 changes: 61 additions & 0 deletions plugins/rbac/src/api/LicensedUsersClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import {
ConfigApi,
createApiRef,
IdentityApi,
} from '@backstage/core-plugin-api';

export type LicensedUsersAPI = {
isLicensePluginEnabled(): Promise<boolean>;
downloadStatistics: () => Promise<Response>;
};

// @public
export const licensedUsersApiRef = createApiRef<LicensedUsersAPI>({
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<boolean> {
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<Response> {
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;
}
}
66 changes: 66 additions & 0 deletions plugins/rbac/src/components/DownloadUserStatistics.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLAnchorElement, 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 (
<a
href="/download-csv"
onClick={handleDownload}
className={classes.linkStyle}
>
Download User List
</a>
);
}

export default DownloadCSVLink;
18 changes: 18 additions & 0 deletions plugins/rbac/src/components/RbacPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<typeof useRoles>;

const mockUseCheckIfLicensePluginEnabled =
useCheckIfLicensePluginEnabled as jest.MockedFunction<
typeof useCheckIfLicensePluginEnabled
>;

const RequirePermissionMock = RequirePermission as jest.MockedFunction<
typeof RequirePermission
>;
Expand All @@ -46,6 +56,14 @@ describe('RbacPage', () => {
createRoleAllowed: false,
createRoleLoading: false,
});
mockUseCheckIfLicensePluginEnabled.mockReturnValue({
loading: false,
isEnabled: false,
licenseCheckError: {
message: '',
name: '',
},
});
await renderInTestApp(<RbacPage />);
expect(screen.getByText('Administration')).toBeInTheDocument();
});
Expand Down
17 changes: 17 additions & 0 deletions plugins/rbac/src/components/RolesList/RolesList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Expand Down Expand Up @@ -53,6 +58,10 @@ const mockUsePermission = usePermission as jest.MockedFunction<
>;

const mockUseRoles = useRoles as jest.MockedFunction<typeof useRoles>;
const mockUseCheckIfLicensePluginEnabled =
useCheckIfLicensePluginEnabled as jest.MockedFunction<
typeof useCheckIfLicensePluginEnabled
>;

const RequirePermissionMock = RequirePermission as jest.MockedFunction<
typeof RequirePermission
Expand All @@ -74,6 +83,14 @@ describe('RolesList', () => {
createRoleAllowed: false,
createRoleLoading: false,
});
mockUseCheckIfLicensePluginEnabled.mockReturnValue({
loading: false,
isEnabled: false,
licenseCheckError: {
message: '',
name: '',
},
});
const { queryByText } = await renderInTestApp(<RolesList />);
expect(queryByText('All roles (2)')).not.toBeNull();
expect(queryByText('role:default/guests')).not.toBeNull();
Expand Down
10 changes: 9 additions & 1 deletion plugins/rbac/src/components/RolesList/RolesList.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -68,6 +70,11 @@ export const RolesList = () => {

const errorWarning = getErrorWarning();

const isLicensePluginEnabled = useCheckIfLicensePluginEnabled();
if (isLicensePluginEnabled.loading) {
return <Progress />;
}

return (
<>
<SnackbarAlert toastMessage={toastMessage} onAlertClose={onAlertClose} />
Expand Down Expand Up @@ -101,6 +108,7 @@ export const RolesList = () => {
</div>
}
/>
{isLicensePluginEnabled.isEnabled && <DownloadCSVLink />}
{openDialog && (
<DeleteRoleDialog
open={openDialog}
Expand Down
24 changes: 24 additions & 0 deletions plugins/rbac/src/hooks/useCheckIfLicensePluginEnabled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useAsync } from 'react-use';

import { useApi } from '@backstage/core-plugin-api';

import { licensedUsersApiRef } from '../api/LicensedUsersClient';

export const useCheckIfLicensePluginEnabled = (): {
loading: boolean;
isEnabled: boolean | undefined;
licenseCheckError: Error;
} => {
const licensedUsersClient = useApi(licensedUsersApiRef);
const {
value: isEnabled,
loading,
error: licenseCheckError,
} = useAsync(async () => await licensedUsersClient.isLicensePluginEnabled());

return {
loading,
isEnabled,
licenseCheckError: licenseCheckError as Error,
};
};
13 changes: 13 additions & 0 deletions plugins/rbac/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 }),
}),
],
});

Expand Down

0 comments on commit b3506ff

Please sign in to comment.