Skip to content

Commit

Permalink
Merge branch 'main' into feat/admin-dashboard-num-employers
Browse files Browse the repository at this point in the history
  • Loading branch information
banders committed Sep 27, 2024
2 parents 8088101 + 22ce283 commit f703035
Show file tree
Hide file tree
Showing 31 changed files with 753 additions and 673 deletions.
18 changes: 18 additions & 0 deletions .diagrams/architecture/announcements/states.mermaid
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
config:
layout: elk
elk:
nodePlacementStrategy: NETWORK_SIMPLEX
---
flowchart TD
draft>draft]
published>published]
expired>expired]
archived>archived]

published --Save as--> published
expired & draft --Save as--> draft & published
published -.On<br>expires_on<br>date.-> expired
draft & published & expired --Archive--> archived
archived & expired -.Scheduled<br>task.-> delete[Delete after 90 days]
published --Unpublish--> draft
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions .diagrams/architecture/announcements/user-actions.mermaid
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
flowchart TD
start([Admin User])

%% Nodes - Actions
active[Publicly available]
email[Send email 14 days before expire_on date]
scheduleExpire[Automatically expire announcement after expire_on date]
scheduleDelete[Automatically delete announcement after 90 days]

%% Nodes - decisions
choice{Actions}
saveAny{Save As}
saveP{Save As}

%% Nodes - Data objects
draft>fa:fa-file Draft Announcement]
published>fa:fa-file Published Announcement]
expired>fa:fa-file Expired Announcement]
archive>fa:fa-file Archived Announcement]

%% New workflow
start --> choice
choice --New--> saveAny

%% Existing workflows
choice --Edit draft/expired--> saveAny
choice --Edit published--> saveP
choice --Unpublish--> draft
published -.Scheduled task.-> email

%% Saving workflow
saveAny --Draft--> draft
saveAny & saveP --Publish--> published
published -.After active_on date.-> active

%% Expire workflows
published -.Scheduled task.-> scheduleExpire
scheduleExpire --> expired

%% Delete workflows
choice --Archive--> archive
expired & archive -.Scheduled task.-> scheduleDelete
4 changes: 1 addition & 3 deletions .github/workflows/.deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,7 @@ jobs:
--set-string global.secrets.s3Endpoint="${{secrets.S3_ENDPOINT }}" \
--set-string global.secrets.s3Bucket="${{secrets.S3_BUCKET }}" \
--set-string global.secrets.powerBiAnalyticsWorkspaceId="${{secrets.POWERBI_ANALYTICS_WORKSPACE_ID }}" \
--set-string global.secrets.powerBiAnalyticsSubmissionAnalyticsId="${{secrets.POWERBI_ANALYTICS_SUBMISSIONANALYTICS_ID }}" \
--set-string global.secrets.powerBiAnalyticsUserBehaviourId="${{secrets.POWERBI_ANALYTICS_USERBEHAVIOUR_ID }}" \
--set-string global.secrets.powerBiAnalyticsDataAnalyticsId="${{secrets.POWERBI_ANALYTICS_DATAANALYTICS_ID }}" \
--set-string global.secrets.powerBiAnalyticsReportId="${{secrets.POWERBI_ANALYTICS_REPORT_ID }}" \
--set-string global.serverAdminFrontend="${{ inputs.admin-frontend-url }}" \
--set-string global.serverFrontend="${{ inputs.frontend-url }}" \
--set-string crunchy.pgBackRest.s3.bucket="${{ secrets.S3_BUCKET }}" \
Expand Down
32 changes: 22 additions & 10 deletions admin-frontend/src/components/AnalyticsPage.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<div v-if="isAnalyticsAvailable">
<div v-if="isAnalyticsAvailable" class="w-100 overflow-x-auto">
<div
v-for="[name, details] in resourceDetails"
:key="name"
Expand All @@ -20,10 +20,11 @@
import { PowerBIReportEmbed } from 'powerbi-client-vue-js';
import { EventHandler } from 'powerbi-client-vue-js/dist/types/src/utils/utils';
import { models, IReportEmbedConfiguration, Report } from 'powerbi-client';
import { reactive, CSSProperties } from 'vue';
import { reactive, CSSProperties, Reactive } from 'vue';
import ApiService from '../services/apiService';
import { ZonedDateTime, Duration } from '@js-joda/core';
import { POWERBI_RESOURCE } from '../utils/constant';
import { NotificationService } from '../services/notificationService';
type PowerBiDetails = {
config: IReportEmbedConfiguration;
Expand All @@ -36,17 +37,17 @@ const isAnalyticsAvailable =
(window as any).config?.IS_ADMIN_ANALYTICS_AVAILABLE?.toUpperCase() == 'TRUE';
const resourceDetails = createDefaultPowerBiDetailsMap([
POWERBI_RESOURCE.SUBMISSIONANALYTICS,
POWERBI_RESOURCE.USERBEHAVIOUR,
POWERBI_RESOURCE.DATAANALYTICS,
POWERBI_RESOURCE.ANALYTICS,
]);
if (isAnalyticsAvailable) {
getPowerBiAccessToken(resourceDetails);
}
/** Create a Map containing the details of the resources. */
function createDefaultPowerBiDetailsMap(resourcesToLoad: POWERBI_RESOURCE[]) {
function createDefaultPowerBiDetailsMap(
resourcesToLoad: POWERBI_RESOURCE[],
): Reactive<Map<POWERBI_RESOURCE, PowerBiDetails>> {
const resourceDetails = reactive(new Map<POWERBI_RESOURCE, PowerBiDetails>());
for (const name of resourcesToLoad) {
Expand Down Expand Up @@ -97,10 +98,21 @@ function createDefaultPowerBiDetailsMap(resourcesToLoad: POWERBI_RESOURCE[]) {
* Get the embed config from the service.
* Auto refresh the token before it expires.
*/
async function getPowerBiAccessToken(resourceDetails) {
const embedInfo = await ApiService.getPowerBiEmbedAnalytics(
Array.from(resourceDetails.keys()),
);
async function getPowerBiAccessToken(
resourceDetails: Reactive<Map<POWERBI_RESOURCE, PowerBiDetails>>,
) {
let embedInfo;
try {
embedInfo = await ApiService.getPowerBiEmbedAnalytics(
Array.from(resourceDetails.keys()),
);
} catch (err) {
NotificationService.pushNotificationError(
'Analytics failed to load. Please try again later or contact the helpdesk.',
undefined,
1000 * 60 * 3,
);
}
for (let resource of embedInfo.resources) {
const ref = resourceDetails.get(resource.name);
if (!ref) continue;
Expand Down
18 changes: 15 additions & 3 deletions admin-frontend/src/components/announcements/AnnouncementForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,15 @@
counter
rows="3"
:error-messages="errors.description"
></v-textarea>
>
<template #counter="{ max }">
<span>
{{ getDescriptionLength(announcementDescription) }}/{{
max
}}
</span>
</template>
</v-textarea>
</v-col>
<v-col cols="12" md="12" sm="12">
<h5 class="mb-2">Time settings</h5>
Expand Down Expand Up @@ -463,7 +471,7 @@ const { handleSubmit, setErrors, errors, meta, values } = useForm({
description(value) {
if (!value) return 'Description is required.';
if (value.length > 2000)
if (getDescriptionLength(value) > 2000)
return 'Description should have a maximum of 2000 characters.';
return true;
Expand Down Expand Up @@ -526,7 +534,7 @@ const { handleSubmit, setErrors, errors, meta, values } = useForm({
const { value: announcementTitle } = useField('title');
const { value: status } = useField<string>('status');
const { value: announcementDescription } = useField('description');
const { value: announcementDescription } = useField<string>('description');
const { value: activeOn } = useField('active_on') as any;
const { value: expiresOn } = useField('expires_on') as any;
const { value: noExpiry } = useField('no_expiry') as any;
Expand All @@ -535,6 +543,10 @@ const { value: linkDisplayName } = useField('linkDisplayName') as any;
const { value: fileDisplayName } = useField('fileDisplayName') as any;
const { value: attachment } = useField('attachment') as any;
const getDescriptionLength = (value: string) => {
return value.replace(/(\r\n|\n|\r)/g, ' ').length;
};
watch(noExpiry, () => {
if (noExpiry.value) {
expiresOn.value = undefined;
Expand Down
4 changes: 1 addition & 3 deletions admin-frontend/src/utils/constant.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,5 @@ export const REPORT_STATUS = Object.freeze({
});

export const POWERBI_RESOURCE = Object.freeze({
SUBMISSIONANALYTICS: 'SubmissionAnalytics',
USERBEHAVIOUR: 'UserBehaviour',
DATAANALYTICS: 'DataAnalytics',
ANALYTICS: 'Analytics',
});
5 changes: 1 addition & 4 deletions backend/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,7 @@ config.defaults({
powerBiUrl: process.env.BACKEND_POWERBI_URL,
analytics: {
workspaceId: process.env.POWERBI_ANALYTICS_WORKSPACE_ID,
submissionAnalyticsId:
process.env.POWERBI_ANALYTICS_SUBMISSIONANALYTICS_ID,
userBehaviourId: process.env.POWERBI_ANALYTICS_USERBEHAVIOUR_ID,
dataAnalyticsId: process.env.POWERBI_ANALYTICS_DATAANALYTICS_ID,
analyticsId: process.env.POWERBI_ANALYTICS_REPORT_ID,
},
},
s3: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jest.mock('../v1/services/utils-service', () => ({

const mockExpireAnnouncements = jest.fn();
jest.mock('../v1/services/announcements-service', () => ({
expireAnnouncements: () => mockExpireAnnouncements(),
announcementService: { expireAnnouncements: () => mockExpireAnnouncements() },
}));

jest.mock('cron', () => ({
Expand Down
4 changes: 2 additions & 2 deletions backend/src/schedulers/expire-announcements-scheduler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import advisoryLock from 'advisory-lock';
import { config } from '../config';
import { logger as log } from '../logger';
import { expireAnnouncements } from '../v1/services/announcements-service';
import { announcementService } from '../v1/services/announcements-service';
import { createJob } from './create-job';

const mutex = advisoryLock(config.get('server:databaseUrl'))(
Expand All @@ -14,7 +14,7 @@ export default createJob(
crontime,
async () => {
log.info('Starting expireAnnounements scheduled job.');
await expireAnnouncements();
await announcementService.expireAnnouncements();
log.info('Completed expireAnnounements scheduled job.');
},
mutex,
Expand Down
6 changes: 4 additions & 2 deletions backend/src/v1/middlewares/validations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ export type UseValidateOptions = {
schema: ZodSchema;
};

export const useValidate = ({ mode, schema }: UseValidateOptions) => {
export const useValidate = ({ mode, schema, }: UseValidateOptions) => {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const data = req[mode];
const results = await schema.parseAsync(data);
req[mode] = results;
next();
} catch (error) {
logger.error(error);
const { path, method } = req;
const errorMessage = `${method} - ${path} - Data validation failed`;
logger.error(errorMessage, error);
return next(error);
}
};
Expand Down
22 changes: 13 additions & 9 deletions backend/src/v1/routes/admin-user-invites-routes.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { faker } from '@faker-js/faker';
import { Application } from 'express';
import express, { Application } from 'express';
import bodyParser from 'body-parser';
import request from 'supertest';
import { UserInputError } from '../types/errors';
import routes from '../routes/admin-user-invites-routes';

const mockDeleteInvite = jest.fn();
const mockGetPendingInvites = jest.fn();
const mockCreateInvite = jest.fn();
const mockResendInvite = jest.fn();
jest.mock('../services/admin-user-invites-service', () => ({
deleteInvite: (...args) => mockDeleteInvite(...args),
getPendingInvites: (...args) => mockGetPendingInvites(...args),
createInvite: (...args) => mockCreateInvite(...args),
resendInvite: (...args) => mockResendInvite(...args),
adminUserInvitesService: {
deleteInvite: (...args) => mockDeleteInvite(...args),
getPendingInvites: (...args) => mockGetPendingInvites(...args),
createInvite: (...args) => mockCreateInvite(...args),
resendInvite: (...args) => mockResendInvite(...args),
},
}));

jest.mock('../middlewares/authorization/authorize', () => ({
Expand All @@ -38,10 +42,10 @@ describe('admin-user-invites-routes', () => {

beforeEach(() => {
jest.clearAllMocks();
app = require('express')();
app.use(require('body-parser').json());
app.use(require('../routes/admin-user-invites-routes').default);
app.use((err, req, res, next) => {
app = express();
app.use(bodyParser.json());
app.use(routes);
app.use((err, req, res, _) => {
res.status(400).send({ error: err.message });
});
});
Expand Down
15 changes: 5 additions & 10 deletions backend/src/v1/routes/admin-user-invites-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,7 @@ import {
AddNewUserSchema,
AddNewUserType,
} from '../middlewares/validations/schemas';
import {
createInvite,
deleteInvite,
getPendingInvites,
resendInvite,
} from '../services/admin-user-invites-service';
import { adminUserInvitesService } from '../services/admin-user-invites-service';
import { utils } from '../services/utils-service';
import { UserInputError } from '../types/errors';

Expand All @@ -22,7 +17,7 @@ router.use(authorize([PTRT_ADMIN_ROLE_NAME]));

router.get('', async (req: Request, res: Response) => {
try {
const invites = await getPendingInvites();
const invites = await adminUserInvitesService.getPendingInvites();
return res.status(200).json(invites);
} catch (error) {
logger.error(error);
Expand All @@ -39,7 +34,7 @@ router.post(
const userInfo = utils.getSessionUser(req);
const jwtPayload = jsonwebtoken.decode(userInfo.jwt) as JwtPayload;
const idirUserGuid = jwtPayload?.idir_user_guid;
await createInvite(
await adminUserInvitesService.createInvite(
email.trim().toLowerCase(),
role,
firstName,
Expand All @@ -59,7 +54,7 @@ router.post(
router.patch('/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
await resendInvite(id);
await adminUserInvitesService.resendInvite(id);
return res.status(200).json({ message: 'Invite resent' });
} catch (error) {
logger.error(error);
Expand All @@ -70,7 +65,7 @@ router.patch('/:id', async (req: Request, res: Response) => {
router.delete('/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params;
await deleteInvite(id);
await adminUserInvitesService.deleteInvite(id);
return res.status(200).json({ message: 'Invite deleted' });
} catch (error) {
logger.error(error);
Expand Down
18 changes: 8 additions & 10 deletions backend/src/v1/routes/analytic-routes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ jest.mock('../services/analytic-service', () => {
const actual = jest.requireActual('../services/analytic-service');
return {
...actual,
getEmbedInfo: (...args) => mockGetEmbedInfo(...args),
analyticsService: {
...actual.analyticsService,
getEmbedInfo: (...args) => mockGetEmbedInfo(...args),
},
};
});

Expand All @@ -23,20 +26,15 @@ describe('analytics', () => {
mockGetEmbedInfo.mockReturnValue({});

it('should return 200', async () => {
await request(app)
.get('/embed?resources[]=SubmissionAnalytics')
.expect(200);
expect(mockGetEmbedInfo).toHaveBeenCalledWith(['SubmissionAnalytics']);
await request(app).get('/embed?resources[]=Analytics').expect(200);
expect(mockGetEmbedInfo).toHaveBeenCalledWith(['Analytics']);
});

it('should allow multiple resource names', async () => {
await request(app)
.get('/embed?resources[]=SubmissionAnalytics&resources[]=DataAnalytics')
.get('/embed?resources[]=Analytics&resources[]=Analytics')
.expect(200);
expect(mockGetEmbedInfo).toHaveBeenCalledWith([
'SubmissionAnalytics',
'DataAnalytics',
]);
expect(mockGetEmbedInfo).toHaveBeenCalledWith(['Analytics', 'Analytics']);
});

it('should return 500 if invalid resource name provided', async () => {
Expand Down
Loading

0 comments on commit f703035

Please sign in to comment.