Skip to content

Commit

Permalink
Merge pull request #60 from icefoganalytics/issue-58/dashboard-page--…
Browse files Browse the repository at this point in the history
…per-user

Part 1: Dashboard Pages (Per-Role) - User Role Dashboard
  • Loading branch information
klondikemarlen authored Mar 8, 2024
2 parents 1cd1b46 + fc3a6e6 commit 507290a
Show file tree
Hide file tree
Showing 31 changed files with 659 additions and 92 deletions.
Binary file modified _Design/Entity Relationship Diagrams.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion _Design/Entity Relationship Diagrams.wsd
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ entity "users" {
division : nvarchar(100)
branch : nvarchar(100)
unit : nvarchar(100)
last_employee_directory_sync_at: datetime2(0)
last_sync_success_at: datetime2(0)
last_sync_failure_at: datetime2(0)
created_at : datetime2(0)
updated_at : datetime2(0)
deleted_at : datetime2(0)
Expand Down
1 change: 1 addition & 0 deletions api/src/controllers/access-requests-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class AccessRequestsController extends BaseController {
],
},
"accessGrant",
{ association: "dataset", include: ["integration"] },
],
limit: this.pagination.limit,
offset: this.pagination.offset,
Expand Down
14 changes: 11 additions & 3 deletions api/src/controllers/datasets-controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { WhereOptions } from "sequelize"
import { isNil } from "lodash"
import { isEmpty, isNil } from "lodash"

import { Dataset } from "@/models"
import { DatasetsPolicy } from "@/policies"
Expand All @@ -11,11 +11,19 @@ import BaseController from "@/controllers/base-controller"
export class DatasetsController extends BaseController {
async index() {
const where = this.query.where as WhereOptions<Dataset>
const filters = this.query.filters as Record<string, unknown>

const scopedDatasets = DatasetsPolicy.applyScope(Dataset, this.currentUser)

const totalCount = await scopedDatasets.count({ where })
const datasets = await scopedDatasets.findAll({
let filteredDatasets = scopedDatasets
if (!isEmpty(filters)) {
Object.entries(filters).forEach(([key, value]) => {
filteredDatasets = filteredDatasets.scope({ method: [key, value] })
})
}

const totalCount = await filteredDatasets.count({ where })
const datasets = await filteredDatasets.findAll({
where,
limit: this.pagination.limit,
offset: this.pagination.offset,
Expand Down
13 changes: 10 additions & 3 deletions api/src/controllers/tags-controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { WhereOptions } from "sequelize"
import { ModelStatic, WhereOptions } from "sequelize"
import { isEmpty } from "lodash"

import { Tag } from "@/models"

Expand All @@ -7,9 +8,15 @@ import BaseController from "@/controllers/base-controller"
export class TagsController extends BaseController {
async index() {
const where = this.query.where as WhereOptions<Tag>
const searchToken = this.query.searchToken as string

const totalCount = await Tag.count({ where })
const tags = await Tag.findAll({
let filteredTags: ModelStatic<Tag> = Tag
if (!isEmpty(searchToken)) {
filteredTags = Tag.scope({ method: ["search", searchToken] })
}

const totalCount = await filteredTags.count({ where })
const tags = await filteredTags.findAll({
where,
limit: this.pagination.limit,
offset: this.pagination.offset,
Expand Down
10 changes: 3 additions & 7 deletions api/src/integrations/yukon-government-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,11 @@ export const yukonGovernmentIntegration = {
})
return data
},
async fetchEmpolyee(email: string): Promise<{
employee: YukonGovernmentEmployee
}> {
async fetchEmpolyee(email: string): Promise<YukonGovernmentEmployee | null> {
const { employees } = await yukonGovernmentIntegration.searchEmployees({ email })

if (isEmpty(employees)) {
throw new Error(
`Failed to find any employee info at https://api.gov.yk.ca/directory/employees?email=${email}`
)
return null
}

if (employees.length > 1) {
Expand All @@ -74,7 +70,7 @@ export const yukonGovernmentIntegration = {
throw new Error(errorMessage)
}

return { employee: employees[0] }
return employees[0]
},
async fetchDepartments(): Promise<{
departments: string[]
Expand Down
31 changes: 31 additions & 0 deletions api/src/models/dataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,37 @@ Dataset.init(
},
}
},
withTagNames(tagNames: string[]) {
return {
include: [
{
association: "tags",
where: {
name: {
[Op.in]: tagNames,
},
},
},
],
}
},
withOwnerDepartment(departmentId: number) {
return {
include: [
{
association: "owner",
include: [
{
association: "groupMembership",
where: {
departmentId,
},
},
],
},
],
}
},
},
}
)
Expand Down
14 changes: 13 additions & 1 deletion api/src/models/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import {
InferCreationAttributes,
Model,
NonAttribute,
Op,
col,
fn,
where,
} from "sequelize"

import sequelize from "@/db/db-client"
Expand Down Expand Up @@ -92,7 +96,15 @@ Tag.init(
deleted_at: null,
},
},
]
],
scopes: {
search(searchToken: string) {
const cleanSearchToken = searchToken.toLowerCase()
return {
where: where(fn("LOWER", col("name")), { [Op.like]: `%${cleanSearchToken}%` }),
}
},
},
}
)

Expand Down
1 change: 1 addition & 0 deletions api/src/models/user-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export enum UserGroupTypes {
UNIT = "unit",
}

export const UNASSIGNED_USER_GROUP_NAME = "Unassigned"
export const DEFAULT_ORDER = -1

export class UserGroup extends Model<
Expand Down
27 changes: 26 additions & 1 deletion api/src/serializers/access-requests/table-serializer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isNil, pick } from "lodash"

import { AccessGrant, AccessRequest, User, UserGroup } from "@/models"
import { AccessGrant, AccessRequest, Dataset, DatasetIntegration, User, UserGroup } from "@/models"
import BaseSerializer from "@/serializers/base-serializer"

export enum AccessRequestTableStatuses {
Expand All @@ -26,6 +26,9 @@ export type AccessRequestTableView = Pick<
requestorDepartmentName: UserGroup["name"]
accessType: AccessGrant["accessType"]
status: AccessRequestTableStatuses
dataset: Pick<Dataset, "id" | "name" | "description"> & {
integration?: Pick<DatasetIntegration, "id" | "status">
}
}

export class TableSerializer extends BaseSerializer<AccessRequest> {
Expand Down Expand Up @@ -56,6 +59,22 @@ export class TableSerializer extends BaseSerializer<AccessRequest> {
if (isNil(accessGrant)) {
throw new Error("AccessRequest must include an access grant.")
}

const { dataset } = this.record
if (isNil(dataset)) {
throw new Error("AccessRequest must include a dataset.")
}

let integrationAttributes = {}
if (!isNil(dataset.integration)) {
integrationAttributes = {
integration: {
id: dataset.integration.id,
status: dataset.integration.status,
},
}
}

return {
...pick(this.record.dataValues, [
"id",
Expand All @@ -71,6 +90,12 @@ export class TableSerializer extends BaseSerializer<AccessRequest> {
requestorDepartmentName: requestorDepartment.name,
accessType: accessGrant.accessType,
status: this.buildStatus(),
dataset: {
id: dataset.id,
name: dataset.name,
description: dataset.description,
...integrationAttributes,
},
}
}

Expand Down
6 changes: 3 additions & 3 deletions api/src/services/users/create-service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isEmpty, isNil } from "lodash"

import db, { Role, User, UserGroup, UserGroupMembership } from "@/models"
import { DEFAULT_ORDER } from "@/models/user-groups"
import { DEFAULT_ORDER, UNASSIGNED_USER_GROUP_NAME } from "@/models/user-groups"

import { Users } from "@/services"
import BaseService from "@/services/base-service"
Expand Down Expand Up @@ -88,10 +88,10 @@ export class CreateService extends BaseService {
const [defaultGroup] = await UserGroup.findOrCreate({
where: {
type: UserGroup.Types.DEPARTMENT,
name: "Unassigned",
name: UNASSIGNED_USER_GROUP_NAME,
},
defaults: {
name: "Unassigned",
name: UNASSIGNED_USER_GROUP_NAME,
type: UserGroup.Types.DEPARTMENT,
order: DEFAULT_ORDER,
},
Expand Down
16 changes: 12 additions & 4 deletions api/src/services/users/yukon-government-directory-sync-service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isNil } from "lodash"

import { User } from "@/models"

import BaseService from "@/services/base-service"
Expand All @@ -19,7 +21,14 @@ export class YukonGovernmentDirectorySyncService extends BaseService {
const email = this.user.email

try {
const { employee } = await yukonGovernmentIntegration.fetchEmpolyee(email)
const employee = await yukonGovernmentIntegration.fetchEmpolyee(email)
if (isNil(employee)) {
console.info(`No employee found with email: ${email} in yukon government directory`)
return this.user.update({
lastSyncFailureAt: new Date(),
})
}

const { department, division, branch, unit } = employee

let userGroup2: UserGroup | null = null
Expand Down Expand Up @@ -74,11 +83,10 @@ export class YukonGovernmentDirectorySyncService extends BaseService {
],
})
} catch (error) {
console.log("Failed to sync user with yukon government directory", error)
await this.user.update({
console.error(`User sync failure for ${email} with yukon government directory`, error)
return this.user.update({
lastSyncFailureAt: new Date(),
})
return this.user
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions web/src/api/access-requests-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import http from "@/api/http-client"

import { AccessGrant } from "@/api/access-grants-api"
import { Dataset } from "@/api/datasets-api"
import { DatasetIntegration } from "@/api/dataset-integrations-api"
import { User } from "@/api/users-api"
import { UserGroup } from "@/api/user-groups-api"

Expand Down Expand Up @@ -46,6 +47,9 @@ export type AccessRequestTableView = Pick<
requestorDepartmentName: UserGroup["name"]
accessType: AccessGrant["accessType"]
status: AccessRequestTableStatuses
dataset: Pick<Dataset, "id" | "name" | "description"> & {
integration?: Pick<DatasetIntegration, "id" | "status">
}
}

export const accessRequestsApi = {
Expand Down
12 changes: 10 additions & 2 deletions web/src/api/base-api.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import qs from "qs"
import qs, { type ParsedQs } from "qs"

export { type ParsedQs }

// See api/src/app.ts -> app.set("query parser", ...)
export function paramsSerializer(params: unknown) {
export function stringifyQuery(params: unknown): string {
return qs.stringify(params, {
arrayFormat: "brackets",
strictNullHandling: true,
})
}

export function parseQuery(search: string): ParsedQs {
return qs.parse(search, {
strictNullHandling: true,
})
}

export type Policy = {
show: boolean
create: boolean
Expand Down
4 changes: 2 additions & 2 deletions web/src/api/http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import axios from "axios"
import { API_BASE_URL } from "@/config"
import auth0 from "@/plugins/auth0-plugin"

import { paramsSerializer } from "@/api/base-api"
import { stringifyQuery } from "@/api/base-api"

export const httpClient = axios.create({
baseURL: API_BASE_URL,
headers: {
"Content-Type": "application/json",
},
paramsSerializer: {
serialize: paramsSerializer,
serialize: stringifyQuery,
},
})

Expand Down
4 changes: 3 additions & 1 deletion web/src/api/tags-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@ export type Tag = {
export const tagsApi = {
async list({
where,
searchToken,
page,
perPage,
}: {
where?: Record<string, unknown> // TODO: consider adding Sequelize types to front-end?
searchToken?: string
page?: number
perPage?: number
} = {}): Promise<{
tags: Tag[]
totalCount: number
}> {
const { data } = await http.get("/api/tags", {
params: { where, page, perPage },
params: { where, searchToken, page, perPage },
})
return data
},
Expand Down
Loading

0 comments on commit 507290a

Please sign in to comment.