Skip to content

Commit

Permalink
Merge pull request #1101 from bcgov/feat/SSOTEAM-1084
Browse files Browse the repository at this point in the history
feat: removed dependency on terraform batch jobs to apply integrations to keycloak
  • Loading branch information
NithinKuruba authored Jan 3, 2024
2 parents 658d864 + dda04e7 commit 1a7b795
Show file tree
Hide file tree
Showing 45 changed files with 1,467 additions and 374 deletions.
8 changes: 6 additions & 2 deletions app/form-components/FormTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ function FormTemplate({ currentUser, request, alert }: Props) {

const handleSubmit = async () => {
try {
const [, err] = await updateRequest(formData, true);
const [data, err] = await updateRequest(formData, true);

if (err) {
alert.show({
Expand All @@ -340,7 +340,11 @@ function FormTemplate({ currentUser, request, alert }: Props) {

router.push({
pathname: isAdmin ? '/admin-dashboard' : '/my-dashboard',
query: { id: formData.id },
query: {
id: data.id,
integrationFailedMessageModal: ['planFailed', 'applyFailed'].includes(data.status!),
requestId: padStart(String(data.id), 8, '0'),
},
});
surveyContext?.setShowSurvey(true, 'createIntegration');
}
Expand Down
27 changes: 20 additions & 7 deletions app/jest/ssoDashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import AdminDashboard from 'pages/admin-dashboard';
import { Integration } from 'interfaces/Request';
import { sampleRequest } from './samples/integrations';
import { deleteRequest, updateRequestMetadata, updateRequest } from 'services/request';
import { deleteRequest, updateRequestMetadata, updateRequest, restoreRequest } from 'services/request';

const sampleSession = {
email: '',
Expand Down Expand Up @@ -37,6 +37,7 @@ jest.mock('services/request', () => {
deleteRequest: jest.fn(() => Promise.resolve([[''], null])),
updateRequestMetadata: jest.fn(() => Promise.resolve([[], null])),
updateRequest: jest.fn(() => Promise.resolve([[], null])),
restoreRequest: jest.fn(() => Promise.resolve([[''], null])),
};
});

Expand Down Expand Up @@ -74,7 +75,7 @@ describe('SSO Dashboard', () => {
});

const firstRow = screen.getByRole('row', {
name: '1 project_name_1 Applied Active Events Edit Delete from Keycloak',
name: '1 project_name_1 Applied Active Events Edit Delete from Keycloak Restore at Keycloak',
});
fireEvent.click(firstRow);
await waitFor(() => {
Expand Down Expand Up @@ -134,7 +135,7 @@ describe('SSO Dashboard', () => {
screen.getByText('project_name_1');
});
const firstRow = screen.getByRole('row', {
name: '1 project_name_1 Applied Active Events Edit Delete from Keycloak',
name: '1 project_name_1 Applied Active Events Edit Delete from Keycloak Restore at Keycloak',
});
fireEvent.click(firstRow);
await waitFor(() => {
Expand Down Expand Up @@ -166,6 +167,18 @@ describe('SSO Dashboard', () => {
await waitFor(() => {
expect(deleteRequest).toHaveBeenCalled();
});

//click on restore icon
const restoreButton = screen.getAllByRole('button', { name: 'Restore at Keycloak' });
fireEvent.click(restoreButton[0]);
await waitFor(() => {
expect(screen.getByTitle('Confirm Restoration')).toBeInTheDocument();
});
const confirmRestoreButton = screen.getAllByTestId('confirm-delete-confirm-restoration');
fireEvent.click(confirmRestoreButton[0]);
await waitFor(() => {
expect(restoreRequest).toHaveBeenCalled();
});
});

it('testing on pagination buttons', async () => {
Expand Down Expand Up @@ -204,7 +217,7 @@ describe('SSO Dashboard', () => {
screen.getByText('project_name_1');
});
const firstRow = screen.getByRole('row', {
name: '1 project_name_1 Applied Active Events Edit Delete from Keycloak',
name: '1 project_name_1 Applied Active Events Edit Delete from Keycloak Restore at Keycloak',
});
fireEvent.click(firstRow);

Expand Down Expand Up @@ -243,7 +256,7 @@ describe('SSO Dashboard', () => {
screen.getByText('project_name_1');
});
const firstRow = screen.getByRole('row', {
name: '1 project_name_1 Applied Active Events Edit Delete from Keycloak',
name: '1 project_name_1 Applied Active Events Edit Delete from Keycloak Restore at Keycloak',
});
fireEvent.click(firstRow);

Expand All @@ -268,7 +281,7 @@ describe('SSO Dashboard', () => {
screen.getByText('project_name_1');
});
const firstRow = screen.getByRole('row', {
name: '1 project_name_1 Applied Active Events Edit Delete from Keycloak',
name: '1 project_name_1 Applied Active Events Edit Delete from Keycloak Restore at Keycloak',
});
fireEvent.click(firstRow);

Expand All @@ -293,7 +306,7 @@ describe('SSO Dashboard', () => {
screen.getByText('project_name_1');
});
const firstRow = screen.getByRole('row', {
name: '1 project_name_1 Applied Active Events Edit Delete from Keycloak',
name: '1 project_name_1 Applied Active Events Edit Delete from Keycloak Restore at Keycloak',
});
fireEvent.click(firstRow);

Expand Down
3 changes: 1 addition & 2 deletions app/metadata/options.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
export const workflowStatusOptions = [
{ value: 'draft', label: 'Draft' },
{ value: 'submitted', label: 'Submitted' },
{ value: 'pr', label: 'PR' },
{ value: 'prFailed', label: 'PR Failed' },
{ value: 'planned', label: 'Planned' },
{ value: 'planFailed', label: 'Plan Failed' },
{ value: 'applied', label: 'Applied' },
{ value: 'applyFailed', label: 'Apply Failed' },
];
34 changes: 29 additions & 5 deletions app/page-partials/my-dashboard/IntegrationInfoTabs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import GithubStatusPanel from './GithubStatusPanel';
import ServiceAccountRoles from 'page-partials/my-dashboard/ServiceAccountRoles';
import DigitalCredentialPanel from './DigitalCredentialPanel';
import MetricsPanel from './MetricsPanel';
import { ErrorMessage } from '@app/components/MessageBox';

const TabWrapper = styled.div<{ short?: boolean }>`
padding-left: 1rem;
Expand Down Expand Up @@ -77,6 +78,25 @@ const IntegrationWrapper = ({ integration, children }: { integration: Integratio
);
};

const getIntegrationErrorTab = () => {
return (
<Tab key={TAB_DETAILS} tab="Technical Details">
<TabWrapper short={false}>
<div style={{ display: 'inline-flex', margin: '20px 0 20px 0', background: '#FFCCCB', borderRadius: '5px' }}>
<div style={{ padding: 5 }}>
<ErrorMessage>
Your request for an integration could not be completed. Please{' '}
<Link external href="mailto:[email protected]">
contact the Pathfinder SSO Team
</Link>
</ErrorMessage>
</div>
</div>
</TabWrapper>
</Tab>
);
};

const getInstallationTab = ({
integration,
approvalContext,
Expand Down Expand Up @@ -304,17 +324,21 @@ function IntegrationInfoTabs({ integration }: Props) {
integration.devIdps?.length && integration.devIdps?.every((idp) => idp === 'digitalcredential');

if (displayStatus === 'Submitted') {
if (bceidProdApplying || githubProdApplying || digitalCredentialProdApplying) {
tabs.push(getApprovalProgressTab({ integration, approvalContext }));
if (['planFailed', 'applyFailed'].includes(integration.status as string)) {
tabs.push(getIntegrationErrorTab());
allowedTabs.push(TAB_DETAILS);
} else {
tabs.push(getProgressTab({ integration, approvalContext }));
allowedTabs.push(TAB_DETAILS);
if (bceidProdApplying || githubProdApplying || digitalCredentialProdApplying) {
tabs.push(getApprovalProgressTab({ integration, approvalContext }));
allowedTabs.push(TAB_DETAILS);
} else {
tabs.push(getProgressTab({ integration, approvalContext }));
allowedTabs.push(TAB_DETAILS);
}
}
} else if (displayStatus === 'Completed') {
tabs.push(getInstallationTab({ integration, approvalContext }));
allowedTabs.push(TAB_DETAILS);

// Exclude role management from integrations with only DC
if (!digitalCredentialOnly) {
tabs.push(getRoleManagementTab({ integration }));
Expand Down
41 changes: 39 additions & 2 deletions app/pages/admin-dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import startCase from 'lodash.startcase';
import { faTrash, faEdit, faEye } from '@fortawesome/free-solid-svg-icons';
import { faTrash, faEdit, faEye, faTrashRestoreAlt } from '@fortawesome/free-solid-svg-icons';
import Table from 'components/TableNew';
import { getRequestAll, deleteRequest } from 'services/request';
import { getRequestAll, deleteRequest, restoreRequest } from 'services/request';
import { PageProps } from 'interfaces/props';
import { Integration, Option } from 'interfaces/Request';
import { ActionButtonContainer, ActionButton, VerticalLine } from 'components/ActionButtons';
Expand Down Expand Up @@ -124,6 +124,12 @@ export default function AdminDashboard({ session }: PageProps) {
else return true;
};

const canRestore = (request: Integration) => {
if (request.archived === false) return false;
else if (!['submitted'].includes(request?.status || '')) return false;
else return true;
};

const handleEdit = async (request: Integration) => {
if (!request.id || !canEdit(request)) return;
await router.push(`/request/${request.id}?status=${request.status}`);
Expand All @@ -135,13 +141,26 @@ export default function AdminDashboard({ session }: PageProps) {
window.location.hash = 'delete-modal';
};

const handleRestore = async (request: Integration) => {
if (!request.id || !canRestore(request)) return;
setSelectedId(request.id);
window.location.hash = 'restore-modal';
};

const confirmDelete = async () => {
if (!canDelete) return;
await deleteRequest(selectedId);
await getData();
window.location.hash = '#';
};

const confirmRestore = async () => {
if (!canRestore) return;
await restoreRequest(selectedId);
await getData();
window.location.hash = '#';
};

const activateRow = (request: any) => {
setSelectedId(request['cells'][0].value);
setActivePanel('details');
Expand Down Expand Up @@ -215,6 +234,16 @@ export default function AdminDashboard({ session }: PageProps) {
activeColor={PRIMARY_RED}
title="Delete from Keycloak"
/>
<VerticalLine />
<ActionButton
icon={faTrashRestoreAlt}
role="button"
aria-label="restore"
onClick={() => handleRestore(row)}
disabled={!canRestore(row)}
activeColor={PRIMARY_RED}
title="Restore at Keycloak"
/>
</ActionButtonContainer>
),
};
Expand Down Expand Up @@ -296,6 +325,14 @@ export default function AdminDashboard({ session }: PageProps) {
confirmText="Delete"
title="Confirm Deletion"
/>
<CenteredModal
id="restore-modal"
data-testid="modal-restore-integration"
content="You are about to restore this integration."
onConfirm={confirmRestore}
confirmText="Restore"
title="Confirm Restoration"
/>
</>
);
}
5 changes: 4 additions & 1 deletion app/pages/my-dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ function MyDashboard() {
const router = useRouter();

useEffect(() => {
router.replace('/my-dashboard/integrations');
router.replace({
pathname: '/my-dashboard/integrations',
query: router.query,
});
}, []);

return null;
Expand Down
78 changes: 71 additions & 7 deletions app/pages/my-dashboard/integrations.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,85 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import IntegrationInfoTabs from 'page-partials/my-dashboard/IntegrationInfoTabs';
import IntegrationList from 'page-partials/my-dashboard/IntegrationList';
import VerticalLayout from 'page-partials/my-dashboard/VerticalLayout';
import { Integration } from 'interfaces/Request';
import { PageProps } from 'interfaces/props';
import CenteredModal from '@app/components/CenteredModal';
import { useRouter } from 'next/router';
import { faCommentDots, faEnvelope, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import styled from 'styled-components';
import { Link } from '@button-inc/bcgov-theme';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

const Column = styled.div`
display: flex;
flex-direction: column;
flex-basis: 100%;
flex: 1;
`;

function MyIntegrations({ session }: PageProps) {
const router = useRouter();
const [integration, setIntegration] = useState<Integration | null>(null);
const [integrationCount, setIntegrationCount] = useState(1);
const [showModal, setShowModal] = useState(false);
const integrationFailedMessageModalId = 'integration-failed-modal';
const handleModalFailedMessageModal = async () => (window.location.hash = integrationFailedMessageModalId);
const [processedRequestId, setProcessedRequestId] = useState('');

useEffect(() => {
setProcessedRequestId(router.query.requestId as string);
if (router?.query?.integrationFailedMessageModal === 'true') {
setShowModal(true);
handleModalFailedMessageModal();
}
}, [router.query.showModal]);

return (
<VerticalLayout
tab="integrations"
leftPanel={() => <IntegrationList setIntegration={setIntegration} setIntegrationCount={setIntegrationCount} />}
rightPanel={() => integration && <IntegrationInfoTabs integration={integration} />}
showResizable={integrationCount > 0}
/>
<>
<VerticalLayout
tab="integrations"
leftPanel={() => <IntegrationList setIntegration={setIntegration} setIntegrationCount={setIntegrationCount} />}
rightPanel={() => integration && <IntegrationInfoTabs integration={integration} />}
showResizable={integrationCount > 0}
/>
<CenteredModal
title={`${processedRequestId} - Integration request failed`}
icon={faExclamationTriangle}
id={integrationFailedMessageModalId}
content={
<div>
<div>
<p>The integration request could not be completed. Please contact the Pathfinder SSO Team.</p>
</div>
<div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', width: '100%' }}>
<Column>
<div>
<a
href="https://chat.developer.gov.bc.ca/channel/sso"
target="_blank"
title="Rocket Chat"
style={{ color: '#0d6efd' }}
>
<FontAwesomeIcon size="1x" icon={faCommentDots} color="#0d6efd" /> Rocketchat
</a>
</div>
</Column>
<Column>
<div>
<a href="mailto:[email protected]" title="Pathfinder SSO" style={{ color: '#0d6efd' }}>
<FontAwesomeIcon size="1x" icon={faEnvelope} color="#0d6efd" /> Email
</a>
</div>
</Column>
</div>
</div>
}
showCancel={false}
showConfirm={false}
closable
/>
</>
);
}

Expand Down
9 changes: 9 additions & 0 deletions app/services/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ export const resubmitRequest = async (requestId: number): Promise<[Integration,
}
};

export const restoreRequest = async (requestId?: number): Promise<[Integration, null] | [null, AxiosError]> => {
try {
const result: Integration = await instance.get(`requests/${requestId}/restore`).then((res) => res.data);
return [processRequest(result), null];
} catch (err: any) {
return handleAxiosError(err);
}
};

interface RequestAllData {
searchField: string[];
searchKey: string;
Expand Down
Loading

0 comments on commit 1a7b795

Please sign in to comment.