Skip to content

Commit

Permalink
queries list page (#83)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ellenn-A authored Jul 4, 2024
1 parent 33e91cc commit 06ef2cb
Show file tree
Hide file tree
Showing 18 changed files with 441 additions and 36 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "veritable-ui",
"version": "0.6.11",
"version": "0.6.12",
"description": "UI for Veritable",
"main": "src/index.ts",
"type": "module",
Expand Down
26 changes: 13 additions & 13 deletions public/styles/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ a.side-bar.icon {
background-position: center;
}

a.connections-table.icon {
a.list-table.icon {
padding: 1rem;
text-align: center;
margin-bottom: 0.5rem;
Expand Down Expand Up @@ -226,36 +226,36 @@ a.connections-table.icon {
vertical-align: middle;
}

.main.connections {
.main-list-page {
display: flex;
flex-direction: column;
}

.connections-list-nav,
.connections.header {
.list-nav,
.list-page-header {
display: flex;
flex-direction: row;
color: var(--text-color);
font-size: 1rem;
gap: 2ch;
}

.connections.header > .button {
.list-page-header > .button {
flex-basis: 28ch;
}

.connections.header > *:first-child,
.connections-list-nav > *:first-child {
.list-page-header > *:first-child,
.list-nav > *:first-child {
margin-right: auto;
}

.connections.header {
.list-page-header {
background-color: var(--bg-color);
padding-top: 0.5rem;
padding-bottom: 1rem;
}

.connections.list table {
.list-page table {
width: 100%;
border-radius: 0px;
background-color: #fff;
Expand All @@ -264,7 +264,7 @@ a.connections-table.icon {
border-bottom: 1px solid var(--text-color-sub);
}

.connections.list th {
.list-page th {
text-align: left;
color: var(--text-color);
padding: 1rem 0;
Expand All @@ -274,14 +274,14 @@ a.connections-table.icon {
border-bottom: 1px solid var(--text-color-sub);
}

.connections.list td {
.list-page td {
text-align: left;
height: 3rem;
color: var(--text-color-secondary);
font-size: 0.7rem;
}

.connections.list {
.list-page {
background-color: #fff;
overflow-x: auto;

Expand Down Expand Up @@ -471,7 +471,7 @@ a.connections-table.icon {
justify-content: space-between;
min-height: auto;
}
.connections-list-nav {
.list-nav {
padding: 5px;
}

Expand Down
32 changes: 32 additions & 0 deletions seeds/connections-seed.js → seeds/connections-queries-seed.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,45 @@ export async function prepareVariants() {
return variants10000
}

export async function queryVariants(connections) {
const queryVariants = []
const querySize = 10
for (let i = 0; i < querySize; i++) {
const connection = connections[i % connections.length]
if (i % 3 == 0) {
queryVariants.push({
connection_id: connection.id,
query_type: 'Type A',
status: 'pending_your_input',
})
} else if (i % 3 == 1) {
queryVariants.push({
connection_id: connection.id,
query_type: 'Type B',
status: 'resolved',
})
} else if (i % 3 == 2) {
queryVariants.push({
connection_id: connection.id,
query_type: 'Type A',
status: 'pending_their_input',
})
}
}
return queryVariants
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
export async function seed(knex) {
// Deletes ALL existing entries
await knex('query').del()
await knex('connection').del()
const variants10000 = await prepareVariants()
await knex('connection').insert(variants10000)
const firstTenConnections = await knex('connection').orderBy('created_at', 'asc').limit(10)
const variants = await queryVariants(firstTenConnections)
await knex('query').insert(variants)
}
25 changes: 23 additions & 2 deletions src/controllers/queries/__tests__/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,41 @@
import { Readable } from 'node:stream'
import { pino } from 'pino'
import { ILogger } from '../../../logger.js'
import Database from '../../../models/db/index.js'
import QueriesTemplates from '../../../views/queries.js'
import QueryListTemplates from '../../../views/queriesList.js'

type QueryStatus = 'resolved' | 'pending_your_input' | 'pending_their_input'

interface Query {
company_name: string
query_type: string
updated_at: Date
status: QueryStatus
}
function templateFake(templateName: string) {
return Promise.resolve(`${templateName}_template`)
}
function templateListFake(templateName: string, ...args: any[]) {
return Promise.resolve([templateName, args.join('-'), templateName].join('_'))
}
export const withQueriesMocks = () => {
const templateMock = {
const queryTemplateMock = {
chooseQueryPage: () => templateFake('queries'),
} as QueriesTemplates
const queryListTemplateMock = {
listPage: (queries: Query[]) => templateListFake('list', queries[0].company_name, queries[0].status),
} as QueryListTemplates
const mockLogger: ILogger = pino({ level: 'silent' })
const dbMock = {
get: () => Promise.resolve([{ company_name: 'foo', status: 'verified' }]),
} as unknown as Database

return {
templateMock,
queryListTemplateMock,
queryTemplateMock,
mockLogger,
dbMock,
}
}

Expand Down
20 changes: 18 additions & 2 deletions src/controllers/queries/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,26 @@ describe('QueriesController', () => {
})
describe('queries', () => {
it('should match the snapshot of the rendered query page', async () => {
const { mockLogger, templateMock } = withQueriesMocks()
const controller = new QueriesController(templateMock, mockLogger)
const { mockLogger, queryTemplateMock, dbMock, queryListTemplateMock } = withQueriesMocks()
const controller = new QueriesController(queryTemplateMock, queryListTemplateMock, dbMock, mockLogger)
const result = await controller.queries().then(toHTMLString)
expect(result).to.equal('queries_template')
})
it('should call db as expected', async () => {
const { mockLogger, queryTemplateMock, dbMock, queryListTemplateMock } = withQueriesMocks()
const controller = new QueriesController(queryTemplateMock, queryListTemplateMock, dbMock, mockLogger)
const spy = sinon.spy(dbMock, 'get')
await controller.queryManagement().then(toHTMLString)
expect(spy.calledWith('query', {}, [['updated_at', 'desc']])).to.equal(true)
})
it('should call db as expected', async () => {
const { mockLogger, queryTemplateMock, dbMock, queryListTemplateMock } = withQueriesMocks()
const controller = new QueriesController(queryTemplateMock, queryListTemplateMock, dbMock, mockLogger)
const spy = sinon.spy(dbMock, 'get')
await controller.queryManagement('VER123').then(toHTMLString)
const search = 'VER123'
const query = search ? [['company_name', 'ILIKE', `%${search}%`]] : {}
expect(spy.calledWith('query', query, [['updated_at', 'desc']])).to.equal(true)
})
})
})
51 changes: 49 additions & 2 deletions src/controllers/queries/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { Get, Produces, Route, Security, SuccessResponse } from 'tsoa'
import { Get, Produces, Query, Route, Security, SuccessResponse } from 'tsoa'
import { inject, injectable, singleton } from 'tsyringe'

import { Logger, type ILogger } from '../../logger.js'

import Database from '../../models/db/index.js'
import { ConnectionRow, QueryRow } from '../../models/db/types.js'
import QueriesTemplates from '../../views/queries.js'
import QueryListTemplates from '../../views/queriesList.js'
import { HTML, HTMLController } from '../HTMLController.js'

type QueryStatus = 'resolved' | 'pending_your_input' | 'pending_their_input'
interface Query {
company_name: string
query_type: string
updated_at: Date
status: QueryStatus
}

@singleton()
@injectable()
@Security('oauth2')
Expand All @@ -14,6 +25,8 @@ import { HTML, HTMLController } from '../HTMLController.js'
export class QueriesController extends HTMLController {
constructor(
private queriesTemplates: QueriesTemplates,
private queryManagementTemplates: QueryListTemplates,
private db: Database,
@inject(Logger) private logger: ILogger
) {
super()
Expand All @@ -24,10 +37,44 @@ export class QueriesController extends HTMLController {
* Retrieves the query page
*/
@SuccessResponse(200)
@Get('/')
@Get('/new')
public async queries(): Promise<HTML> {
this.logger.debug('query page requested')

return this.html(this.queriesTemplates.chooseQueryPage())
}
/**
* Retrieves the queries page
*/
@SuccessResponse(200)
@Get('/')
public async queryManagement(@Query() search?: string): Promise<HTML> {
this.logger.debug('query management page requested')
const query = search ? [['company_name', 'ILIKE', `%${search}%`]] : {}
const query_subset = await this.db.get('query', query, [['updated_at', 'desc']])
const connections = await this.db.get('connection', query, [['updated_at', 'desc']])
const queries: Query[] = combineData(query_subset, connections)

this.setHeader('HX-Replace-Url', search ? `/queries?search=${encodeURIComponent(search)}` : `/queries`)
return this.html(this.queryManagementTemplates.listPage(queries, search))
}
}

function combineData(query_subset: QueryRow[], connections: ConnectionRow[]): Query[] {
const connectionMap: Record<string, string> = {}
for (const connection of connections) {
if (connection.id) {
connectionMap[connection.id] = connection.company_name
}
}
return query_subset.map((query) => {
const company_name = connectionMap[query.connection_id] || 'Unknown'

return {
company_name: company_name,
query_type: query.query_type,
updated_at: query.updated_at,
status: query.status,
}
})
}
26 changes: 26 additions & 0 deletions src/models/db/migrations/20240628090906_query_table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Knex } from 'knex'

export async function up(knex: Knex): Promise<void> {
const now = () => knex.fn.now()

await knex.schema.createTable('query', (def) => {
def.uuid('id').defaultTo(knex.raw('uuid_generate_v4()')).primary()
def.uuid('connection_id').notNullable()
def.string('query_type').notNullable()
def
.enum('status', ['resolved', 'pending_your_input', 'pending_their_input'], {
useNative: true,
enumName: 'query_status',
})
.notNullable()
def.datetime('created_at').notNullable().defaultTo(now())
def.datetime('updated_at').notNullable().defaultTo(now())

def.foreign('connection_id').references('id').inTable('connection').onDelete('CASCADE').onUpdate('CASCADE')
})
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTable('query')
await knex.schema.raw('DROP TYPE query_status')
}
19 changes: 18 additions & 1 deletion src/models/db/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Knex } from 'knex'
import { z } from 'zod'

export const tablesList = ['connection', 'connection_invite'] as const
export const tablesList = ['connection', 'connection_invite', 'query'] as const

const insertConnection = z.object({
company_name: z.string(),
Expand All @@ -24,6 +24,13 @@ const insertConnectionInvite = z.object({
pin_hash: z.string(),
expires_at: z.date(),
})
const insertQuery = z.object({
connection_id: z.string(),
query_type: z.string(),
status: z.enum(['resolved', 'pending_your_input', 'pending_their_input']),
created_at: z.date(),
updated_at: z.date(),
})

const Zod = {
connection: {
Expand All @@ -42,12 +49,22 @@ const Zod = {
updated_at: z.date(),
}),
},
query: {
insert: insertQuery,
get: insertQuery.extend({
id: z.string(),
created_at: z.date(),
updated_at: z.date(),
}),
},
}

const { connection } = Zod
const { query } = Zod

export type InsertConnection = z.infer<typeof connection.insert>
export type ConnectionRow = z.infer<typeof connection.get>
export type QueryRow = z.infer<typeof query.get>

export type TABLES_TUPLE = typeof tablesList
export type TABLE = TABLES_TUPLE[number]
Expand Down
Loading

0 comments on commit 06ef2cb

Please sign in to comment.