From 07c649a60847722974d9236229febdfd2868d932 Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Tue, 14 May 2024 17:22:09 +0200 Subject: [PATCH 01/14] tmp --- packages/logs/lib/client.ts | 2 +- packages/logs/lib/models/messages.ts | 5 +- .../server/lib/controllers/sync.controller.ts | 2 +- .../v1/logs/getOperation.integration.test.ts | 147 ++++++++++++++++++ .../lib/controllers/v1/logs/getOperation.ts | 49 ++++++ packages/server/lib/routes.ts | 2 + packages/server/lib/utils/tests.ts | 13 +- packages/types/lib/api.endpoints.ts | 4 +- packages/types/lib/api.ts | 3 +- packages/types/lib/logs/api.ts | 13 +- packages/webapp/src/hooks/useLogs.tsx | 16 +- packages/webapp/src/pages/Logs/Search.tsx | 2 +- packages/webapp/src/pages/Logs/Show.tsx | 84 ++++++++-- .../pages/Logs/components/OperationRow.tsx | 15 +- .../pages/Logs/components/OperationTag.tsx | 10 +- .../Logs/components/SearchInOperation.tsx | 12 ++ .../src/pages/Logs/components/StatusTag.tsx | 10 +- packages/webapp/src/pages/Logs/constants.tsx | 4 +- packages/webapp/src/utils/utils.tsx | 8 +- 19 files changed, 354 insertions(+), 47 deletions(-) create mode 100644 packages/server/lib/controllers/v1/logs/getOperation.integration.test.ts create mode 100644 packages/server/lib/controllers/v1/logs/getOperation.ts create mode 100644 packages/webapp/src/pages/Logs/components/SearchInOperation.tsx diff --git a/packages/logs/lib/client.ts b/packages/logs/lib/client.ts index a0e8e73e31..061a80f387 100644 --- a/packages/logs/lib/client.ts +++ b/packages/logs/lib/client.ts @@ -62,7 +62,7 @@ export class LogContext { await this.log({ type: 'log', level: 'warn', message, meta, source: 'internal' }); } - async error(message: string, meta: (MessageMeta & { error?: unknown }) | null = null): Promise { + async error(message: string, meta: (MessageMeta & { error?: unknown; err?: never; e?: never }) | null = null): Promise { const { error, ...rest } = meta || {}; const err = error ? { name: 'Unknown Error', message: 'unknown error', ...errorToObject(error) } : null; await this.log({ type: 'log', level: 'error', message, error: err ? { name: err.name, message: err.message } : null, meta: rest, source: 'internal' }); diff --git a/packages/logs/lib/models/messages.ts b/packages/logs/lib/models/messages.ts index bf4697f508..99ad7397bb 100644 --- a/packages/logs/lib/models/messages.ts +++ b/packages/logs/lib/models/messages.ts @@ -2,6 +2,7 @@ import { client } from '../es/client.js'; import type { MessageRow, OperationRow, SearchLogsState } from '@nangohq/types'; import { indexMessages } from '../es/schema.js'; import type { opensearchtypes } from '@opensearch-project/opensearch'; +import { errors } from '@opensearch-project/opensearch'; export interface ListOperations { count: number; @@ -12,6 +13,8 @@ export interface ListMessages { items: MessageRow[]; } +export const ResponseError = errors.ResponseError; + /** * Create one message */ @@ -68,7 +71,7 @@ export async function listOperations(opts: { accountId: number; environmentId?: /** * Get a single operation */ -export async function getOperation(opts: { id: MessageRow['id'] }): Promise { +export async function getOperation(opts: { id: MessageRow['id'] }): Promise { const res = await client.get<{ id: string; _source: MessageRow }>({ index: indexMessages.index, id: opts.id diff --git a/packages/server/lib/controllers/sync.controller.ts b/packages/server/lib/controllers/sync.controller.ts index 934faf2f64..0de7adbe8d 100644 --- a/packages/server/lib/controllers/sync.controller.ts +++ b/packages/server/lib/controllers/sync.controller.ts @@ -426,7 +426,7 @@ class SyncController { } else { span.setTag('nango.error', actionResponse.error); errorManager.errResFromNangoErr(res, actionResponse.error); - await logCtx.error('Failed to trigger action', { err: actionResponse.error }); + await logCtx.error('Failed to trigger action', { error: actionResponse.error }); await logCtx.failed(); span.finish(); diff --git a/packages/server/lib/controllers/v1/logs/getOperation.integration.test.ts b/packages/server/lib/controllers/v1/logs/getOperation.integration.test.ts new file mode 100644 index 0000000000..bddfe89117 --- /dev/null +++ b/packages/server/lib/controllers/v1/logs/getOperation.integration.test.ts @@ -0,0 +1,147 @@ +import { logContextGetter, migrateMapping } from '@nangohq/logs'; +import { multipleMigrations, seeders } from '@nangohq/shared'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { runServer, shouldBeProtected, shouldRequireQueryEnv } from '../../../utils/tests.js'; + +let api: Awaited>; +describe('GET /logs', () => { + beforeAll(async () => { + await multipleMigrations(); + await migrateMapping(); + + api = await runServer(); + }); + afterAll(() => { + api.server.close(); + }); + + it('should be protected', async () => { + const res = await api.fetch('/api/v1/logs/:operationId', { method: 'GET', query: { env: 'dev' }, params: { operationId: '1' } }); + + shouldBeProtected(res); + }); + + it('should enforce env query params', async () => { + const { env } = await seeders.seedAccountEnvAndUser(); + const res = await api.fetch('/api/v1/logs/:operationId', { method: 'GET', token: env.secret_key, params: { operationId: '1' } }); + + shouldRequireQueryEnv(res); + }); + + it('should validate query params', async () => { + const { env } = await seeders.seedAccountEnvAndUser(); + const res = await api.fetch('/api/v1/logs/:operationId', { + method: 'GET', + query: { env: 'dev', foo: 'bar' }, + token: env.secret_key, + params: { operationId: '1' } + }); + + expect(res.json).toStrictEqual({ + error: { + code: 'invalid_query_params', + errors: [ + { + code: 'unrecognized_keys', + message: "Unrecognized key(s) in object: 'foo'", + path: [] + } + ] + } + }); + expect(res.res.status).toBe(400); + }); + + it('should get empty result', async () => { + const { env } = await seeders.seedAccountEnvAndUser(); + const res = await api.fetch('/api/v1/logs/:operationId', { + method: 'GET', + query: { env: 'dev' }, + token: env.secret_key, + params: { operationId: '1' } + }); + + expect(res.res.status).toBe(200); + expect(res.json).toStrictEqual({ + data: [], + pagination: { total: 0 } + }); + }); + + it('should get one result', async () => { + const { env } = await seeders.seedAccountEnvAndUser(); + + const logCtx = await logContextGetter.create( + { message: 'test 1', operation: { type: 'auth' } }, + { account: { id: env.account_id }, environment: { id: env.id } } + ); + await logCtx.info('test info'); + await logCtx.success(); + + const res = await api.fetch(`/api/v1/logs/:operationId`, { + method: 'GET', + query: { env: 'dev' }, + token: env.secret_key, + params: { operationId: logCtx.id } + }); + + expect(res.res.status).toBe(200); + expect(res.json).toStrictEqual({ + data: { + accountId: env.account_id, + accountName: null, + code: null, + configId: null, + configName: null, + connectionId: null, + connectionName: null, + createdAt: expect.toBeIsoDate(), + endedAt: expect.toBeIsoDate(), + environmentId: env.id, + environmentName: null, + error: null, + id: logCtx.id, + jobId: null, + level: 'info', + message: 'test 1', + meta: null, + operation: { + type: 'auth' + }, + parentId: null, + request: null, + response: null, + source: 'internal', + startedAt: expect.toBeIsoDate(), + state: 'success', + syncId: null, + syncName: null, + title: null, + type: 'log', + updatedAt: expect.toBeIsoDate(), + userId: null + } + }); + }); + + it('should not return result from an other account', async () => { + const { account, env } = await seeders.seedAccountEnvAndUser(); + const env2 = await seeders.seedAccountEnvAndUser(); + + const logCtx = await logContextGetter.create({ message: 'test 1', operation: { type: 'auth' } }, { account, environment: env }); + await logCtx.info('test info'); + await logCtx.success(); + + const res = await api.fetch(`/api/v1/logs/:operationId`, { + method: 'GET', + query: { env: 'dev' }, + token: env2.env.secret_key, + params: { operationId: logCtx.id } + }); + + expect(res.res.status).toBe(404); + expect(res.json).toStrictEqual({ + error: { code: 'not_found' } + }); + }); +}); diff --git a/packages/server/lib/controllers/v1/logs/getOperation.ts b/packages/server/lib/controllers/v1/logs/getOperation.ts new file mode 100644 index 0000000000..8787e1fe62 --- /dev/null +++ b/packages/server/lib/controllers/v1/logs/getOperation.ts @@ -0,0 +1,49 @@ +import { z } from 'zod'; +import { asyncWrapper } from '../../../utils/asyncWrapper.js'; +import { requireEmptyQuery, zodErrorToHTTP } from '../../../utils/validation.js'; +import type { GetOperation } from '@nangohq/types'; +import { model, envs } from '@nangohq/logs'; + +const validation = z + .object({ + operationId: z.string().regex(/([0-9]|[a-zA-Z0-9]{20})/) + }) + .strict(); + +export const getOperation = asyncWrapper(async (req, res) => { + if (!envs.NANGO_LOGS_ENABLED) { + res.status(404).send({ error: { code: 'feature_disabled' } }); + return; + } + + const emptyQuery = requireEmptyQuery(req, { withEnv: true }); + if (emptyQuery) { + res.status(400).send({ error: { code: 'invalid_query_params', errors: zodErrorToHTTP(emptyQuery.error) } }); + return; + } + + const val = validation.safeParse(req.params); + if (!val.success) { + res.status(400).send({ + error: { code: 'invalid_uri_params', errors: zodErrorToHTTP(val.error) } + }); + return; + } + + const { environment, account } = res.locals; + try { + const operation = await model.getOperation({ id: val.data.operationId }); + if (operation.accountId !== account.id || operation.environmentId !== environment.id || !operation.operation) { + res.status(404).send({ error: { code: 'not_found' } }); + return; + } + + res.status(200).send({ data: operation }); + } catch (err) { + if (err instanceof model.ResponseError && err.statusCode === 404) { + res.status(404).send({ error: { code: 'not_found' } }); + return; + } + throw err; + } +}); diff --git a/packages/server/lib/routes.ts b/packages/server/lib/routes.ts index 712a5dd7b9..8d744f06dc 100644 --- a/packages/server/lib/routes.ts +++ b/packages/server/lib/routes.ts @@ -32,6 +32,7 @@ import { isCloud, isEnterprise, AUTH_ENABLED, MANAGED_AUTH_ENABLED, isBasicAuthE import { errorManager } from '@nangohq/shared'; import tracer from 'dd-trace'; import { searchLogs } from './controllers/v1/logs/searchLogs.js'; +import { getOperation } from './controllers/v1/logs/getOperation.js'; export const app = express(); @@ -201,6 +202,7 @@ web.route('/api/v1/onboarding/sync-status').post(webAuth, onboardingController.c web.route('/api/v1/onboarding/action').post(webAuth, onboardingController.writeGithubIssue.bind(onboardingController)); web.route('/api/v1/logs/search').post(webAuth, searchLogs); +web.route('/api/v1/logs/:operationId').get(webAuth, getOperation); // Hosted signin if (!isCloud && !isEnterprise) { diff --git a/packages/server/lib/utils/tests.ts b/packages/server/lib/utils/tests.ts index 00e8ee3d82..c6ea4638c5 100644 --- a/packages/server/lib/utils/tests.ts +++ b/packages/server/lib/utils/tests.ts @@ -7,6 +7,12 @@ import { getServerPort } from '@nangohq/shared'; import { app } from '../routes.js'; +function uriParamsReplacer(tpl: string, data: Record) { + return tpl.replace(/\$\(([^)]+)?\)/g, function (_, $2) { + return data[$2]; + }); +} + /** * Type safe API fetch */ @@ -16,10 +22,11 @@ export function apiFetch(baseUrl: string) { TEndpoint extends APIEndpointsPickerWithPath, TMethod extends TEndpoint['Method'], TQuery extends TEndpoint['Querystring'], - TBody extends TEndpoint['Body'] + TBody extends TEndpoint['Body'], + TParams extends TEndpoint['Params'] >( path: TPath, - { method, query, token, body }: { method?: TMethod; query?: TQuery; token?: string; body?: TBody } = {} + { method, query, token, body, params }: { method?: TMethod; query?: TQuery; token?: string; body?: TBody; params?: TParams } = {} ): Promise<{ res: Response; json: APIEndpointsPicker['Reply'] }> { const search = new URLSearchParams(query); const url = new URL(`${baseUrl}${path}?${search.toString()}`); @@ -31,7 +38,7 @@ export function apiFetch(baseUrl: string) { if (body) { headers.append('content-type', 'application/json'); } - const res = await fetch(url, { + const res = await fetch(params ? uriParamsReplacer(url.href, params) : url, { method: method || 'GET', headers, body: body ? JSON.stringify(body) : null diff --git a/packages/types/lib/api.endpoints.ts b/packages/types/lib/api.endpoints.ts index 629ef5e920..3e9df30468 100644 --- a/packages/types/lib/api.endpoints.ts +++ b/packages/types/lib/api.endpoints.ts @@ -1,8 +1,8 @@ import type { EndpointMethod } from './api'; -import type { SearchLogs } from './logs/api'; +import type { GetOperation, SearchLogs } from './logs/api'; import type { GetOnboardingStatus } from './onboarding/api'; -export type APIEndpoints = SearchLogs | GetOnboardingStatus; +export type APIEndpoints = SearchLogs | GetOperation | GetOnboardingStatus; /** * Automatically narrow endpoints type with Method + Path diff --git a/packages/types/lib/api.ts b/packages/types/lib/api.ts index a60889ac51..4e16549834 100644 --- a/packages/types/lib/api.ts +++ b/packages/types/lib/api.ts @@ -15,6 +15,7 @@ export type ResDefaultErrors = | ApiError<'not_found'> | ApiError<'invalid_query_params', ValidationError[]> | ApiError<'invalid_body', ValidationError[]> + | ApiError<'invalid_uri_params', ValidationError[]> | ApiError<'feature_disabled'>; export type EndpointMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; @@ -68,7 +69,7 @@ export interface Endpoint< /** * Response body for any error */ - Errors: ResDefaultErrors | T['Error']; + Errors: T['Error'] extends ApiError ? ResDefaultErrors | T['Error'] : ResDefaultErrors; /** * Response body (success + error) diff --git a/packages/types/lib/logs/api.ts b/packages/types/lib/logs/api.ts index 71c30c51ca..458f615289 100644 --- a/packages/types/lib/logs/api.ts +++ b/packages/types/lib/logs/api.ts @@ -1,4 +1,4 @@ -import type { ApiError, Endpoint } from '../api'; +import type { Endpoint } from '../api'; import type { MessageState, OperationRow } from './messages'; export type SearchLogs = Endpoint<{ @@ -6,7 +6,6 @@ export type SearchLogs = Endpoint<{ Path: '/api/v1/logs/search'; Querystring: { env: string }; Body: { limit?: number; states?: SearchLogsState[] }; - Error: ApiError<'invalid_query_params'>; Success: { data: OperationRow[]; pagination: { total: number }; @@ -16,3 +15,13 @@ export type SearchLogs = Endpoint<{ export type SearchLogsState = 'all' | MessageState; export type SearchLogsData = SearchLogs['Success']['data'][0]; + +export type GetOperation = Endpoint<{ + Method: 'GET'; + Path: `/api/v1/logs/:operationId`; + Querystring: { env: string }; + Params: { operationId: string }; + Success: { + data: OperationRow; + }; +}>; diff --git a/packages/webapp/src/hooks/useLogs.tsx b/packages/webapp/src/hooks/useLogs.tsx index ed796a549c..acbffb9398 100644 --- a/packages/webapp/src/hooks/useLogs.tsx +++ b/packages/webapp/src/hooks/useLogs.tsx @@ -1,5 +1,7 @@ -import type { SearchLogs } from '@nangohq/types'; +import type { GetOperation, SearchLogs } from '@nangohq/types'; import { useEffect, useState } from 'react'; +import useSWR from 'swr'; +import { swrFetcher } from '../utils/api'; export function useSearchLogs(env: string, body: SearchLogs['Body']) { const [loading, setLoading] = useState(false); @@ -39,3 +41,15 @@ export function useSearchLogs(env: string, body: SearchLogs['Body']) { return { data, error, loading }; } + +export function useOperation(env: string, params: GetOperation['Params']) { + const { data, error } = useSWR(`/api/v1/logs/${params.operationId}?env=${env}`, swrFetcher); + + const loading = !data && !error; + + return { + loading, + error, + operation: data?.data + }; +} diff --git a/packages/webapp/src/pages/Logs/Search.tsx b/packages/webapp/src/pages/Logs/Search.tsx index 92af224c39..2ff2404521 100644 --- a/packages/webapp/src/pages/Logs/Search.tsx +++ b/packages/webapp/src/pages/Logs/Search.tsx @@ -63,7 +63,7 @@ export const LogsSearch: React.FC = () => {

You don't have logs yet.

-
Note that logs older than 15days are automatically cleared.
+
Note that logs older than 15 days are automatically cleared.
); diff --git a/packages/webapp/src/pages/Logs/Show.tsx b/packages/webapp/src/pages/Logs/Show.tsx index ce77f210f0..272ce72348 100644 --- a/packages/webapp/src/pages/Logs/Show.tsx +++ b/packages/webapp/src/pages/Logs/Show.tsx @@ -1,41 +1,91 @@ -export const Show: React.FC = () => { - console.log('coucou'); +import { useMemo } from 'react'; +import Info from '../../components/ui/Info'; +import Spinner from '../../components/ui/Spinner'; +import { useOperation } from '../../hooks/useLogs'; +import { useStore } from '../../store'; +import { OperationTag } from './components/OperationTag'; +import { StatusTag } from './components/StatusTag'; +import { elapsedTime } from '../../utils/utils'; +import { Link } from 'react-router-dom'; +import { ExternalLinkIcon } from '@radix-ui/react-icons'; + +export const Show: React.FC<{ operationId: string }> = ({ operationId }) => { + const env = useStore((state) => state.env); + const { operation, loading, error } = useOperation(env, { operationId }); + + const duration = useMemo(() => { + if (!operation) { + return ''; + } + if (!operation.endedAt) { + return 'n/a'; + } + + return elapsedTime(new Date(operation.startedAt!), new Date(operation.endedAt)); + }, [operation]); + + if (loading) { + return ; + } + + if (error || !operation) { + return An error occurred; + } return (

Operation Details

- Timestamp - Feb 20 21:15:42.52 +
Timestamp
+
{operation.startedAt}
- Integration - Feb 20 21:15:42.52 +
Integration
+
+ +
{operation.configName}
+
+ +
+ +
- Connection - Feb 20 21:15:42.52 +
Connection
+
+ +
{operation.connectionName}
+
+ +
+ +
- Duration - Feb 20 21:15:42.52 +
Duration
+
{duration}
- Type - Feb 20 21:15:42.52 +
Type
+
+ +
- Script - Feb 20 21:15:42.52 +
Script
+
{operation.syncName}
- Status - Feb 20 21:15:42.52 +
Status
+
+ +
-

Payload

+

Payload

+ {!operation.meta &&
No payload...
}

Logs

diff --git a/packages/webapp/src/pages/Logs/components/OperationRow.tsx b/packages/webapp/src/pages/Logs/components/OperationRow.tsx index 6197b51ce3..4743715e15 100644 --- a/packages/webapp/src/pages/Logs/components/OperationRow.tsx +++ b/packages/webapp/src/pages/Logs/components/OperationRow.tsx @@ -2,13 +2,15 @@ import type { Row } from '@tanstack/react-table'; import { flexRender } from '@tanstack/react-table'; import type { SearchLogsData } from '@nangohq/types'; -import { Drawer, DrawerContent, DrawerTrigger } from '../../../components/ui/Drawer'; +import { Drawer, DrawerContent, DrawerTrigger, DrawerClose } from '../../../components/ui/Drawer'; import * as Table from '../../../components/ui/Table'; import { Show } from '../Show'; +import { Cross1Icon } from '@radix-ui/react-icons'; +const drawerWidth = '1034px'; export const OperationRow: React.FC<{ row: Row }> = ({ row }) => { return ( - + {row.getVisibleCells().map((cell) => ( @@ -17,8 +19,13 @@ export const OperationRow: React.FC<{ row: Row }> = ({ row }) => -
- +
+
+ + + +
+
diff --git a/packages/webapp/src/pages/Logs/components/OperationTag.tsx b/packages/webapp/src/pages/Logs/components/OperationTag.tsx index 0eb9dd7b63..deba48ee4d 100644 --- a/packages/webapp/src/pages/Logs/components/OperationTag.tsx +++ b/packages/webapp/src/pages/Logs/components/OperationTag.tsx @@ -1,9 +1,15 @@ // import { ChevronRight } from '@geist-ui/icons'; import type { SearchLogsData } from '@nangohq/types'; +import { cn } from '../../../utils/utils'; -export const OperationTag: React.FC<{ operation: Exclude }> = ({ operation }) => { +export const OperationTag: React.FC<{ operation: Exclude; highlight?: boolean }> = ({ operation, highlight }) => { return ( -
+
{operation.type} {/* {'action' in operation && ( <> diff --git a/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx new file mode 100644 index 0000000000..69343adcc4 --- /dev/null +++ b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx @@ -0,0 +1,12 @@ +import { Input } from '../../../components/ui/input/Input'; + +export const SearchInOperation: React.FC<{ operationId: string }> = ({ operationId }) => { + return ( +
+
+ +
+
+
+ ); +}; diff --git a/packages/webapp/src/pages/Logs/components/StatusTag.tsx b/packages/webapp/src/pages/Logs/components/StatusTag.tsx index 33f66692bf..26f4c959f5 100644 --- a/packages/webapp/src/pages/Logs/components/StatusTag.tsx +++ b/packages/webapp/src/pages/Logs/components/StatusTag.tsx @@ -9,31 +9,31 @@ export const StatusTag: React.FC<{ state: SearchLogsData['state'] }> = ({ state ); } else if (state === 'running') { return ( -
+
Running
); } else if (state === 'cancelled') { return ( -
+
Cancelled
); } else if (state === 'failed') { return ( -
+
Failed
); } else if (state === 'timeout') { return ( -
+
Timeout
); } else if (state === 'waiting') { return ( -
+
Waiting
); diff --git a/packages/webapp/src/pages/Logs/constants.tsx b/packages/webapp/src/pages/Logs/constants.tsx index b8b801a52c..c87b8d6f51 100644 --- a/packages/webapp/src/pages/Logs/constants.tsx +++ b/packages/webapp/src/pages/Logs/constants.tsx @@ -1,6 +1,6 @@ import type { ColumnDef } from '@tanstack/react-table'; import type { SearchLogsData, SearchLogsState } from '@nangohq/types'; -import { formatDateToIntFormat } from '../../utils/utils'; +import { formatDateToInternationalFormat } from '../../utils/utils'; import { StatusTag } from './components/StatusTag'; import { OperationTag } from './components/OperationTag'; import type { MultiSelectArgs } from './components/MultiSelect'; @@ -12,7 +12,7 @@ export const columns: ColumnDef[] = [ header: 'Timestamp', size: 150, cell: ({ row }) => { - return formatDateToIntFormat(row.original.createdAt); + return formatDateToInternationalFormat(row.original.createdAt); } }, { diff --git a/packages/webapp/src/utils/utils.tsx b/packages/webapp/src/utils/utils.tsx index 6120e5a226..938ad63097 100644 --- a/packages/webapp/src/utils/utils.tsx +++ b/packages/webapp/src/utils/utils.tsx @@ -92,9 +92,9 @@ export function formatTimestampWithTZ(timestamp: number): string { return formattedDate; } -export function elapsedTime(start: number, end: number): string { - const startTime = new Date(start).getTime(); - const endTime = new Date(end).getTime(); +export function elapsedTime(start: Date | number, end: Date | number): string { + const startTime = start instanceof Date ? start.getTime() : new Date(start).getTime(); + const endTime = end instanceof Date ? end.getTime() : new Date(end).getTime(); if (isNaN(startTime) || isNaN(endTime)) { return ''; @@ -146,7 +146,7 @@ export function formatDateToUSFormat(dateString: string): string { return formattedDate; } -export function formatDateToIntFormat(dateString: string): string { +export function formatDateToInternationalFormat(dateString: string): string { const date = new Date(dateString); const options: Intl.DateTimeFormatOptions = { hour: '2-digit', From a67dd4a28e0b06b6a1494b6969e466455da921c5 Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Wed, 15 May 2024 09:35:41 +0200 Subject: [PATCH 02/14] more --- package-lock.json | 15 ++- packages/logs/lib/models/helpers.ts | 1 + packages/logs/lib/models/messages.ts | 11 ++- .../lib/controllers/v1/logs/getOperation.ts | 4 +- ...s => searchOperations.integration.test.ts} | 56 ++++++++--- .../{searchLogs.ts => searchOperations.ts} | 8 +- packages/server/lib/routes.ts | 5 +- packages/server/lib/utils/tests.ts | 4 +- packages/server/package.json | 1 + packages/types/lib/api.endpoints.ts | 4 +- packages/types/lib/logs/api.ts | 10 +- packages/webapp/src/components/ui/Table.tsx | 2 +- packages/webapp/src/hooks/useLogs.tsx | 12 +-- packages/webapp/src/pages/Logs/Search.tsx | 10 +- packages/webapp/src/pages/Logs/Show.tsx | 15 +-- .../src/pages/Logs/components/MultiSelect.tsx | 12 +-- .../pages/Logs/components/OperationRow.tsx | 4 +- .../pages/Logs/components/OperationTag.tsx | 4 +- .../Logs/components/SearchInOperation.tsx | 95 ++++++++++++++++++- .../src/pages/Logs/components/StatusTag.tsx | 4 +- packages/webapp/src/pages/Logs/constants.tsx | 22 ++--- 21 files changed, 223 insertions(+), 76 deletions(-) rename packages/server/lib/controllers/v1/logs/{searchLogs.integration.test.ts => searchOperations.integration.test.ts} (68%) rename packages/server/lib/controllers/v1/logs/{searchLogs.ts => searchOperations.ts} (82%) diff --git a/package-lock.json b/package-lock.json index 69a3c2aa71..e7f4340de4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32145,6 +32145,7 @@ "@types/simple-oauth2": "^4.1.1", "@types/uuid": "^8.3.4", "@types/ws": "^8.5.4", + "get-port": "7.1.0", "nodemon": "^3.0.1", "typescript": "^5.3.3", "vitest": "0.33.0" @@ -32154,6 +32155,18 @@ "npm": ">=6.14.11" } }, + "packages/server/node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/shared": { "name": "@nangohq/shared", "version": "0.39.27", @@ -34199,4 +34212,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/logs/lib/models/helpers.ts b/packages/logs/lib/models/helpers.ts index 7f4b3b689b..656642c8b9 100644 --- a/packages/logs/lib/models/helpers.ts +++ b/packages/logs/lib/models/helpers.ts @@ -1,6 +1,7 @@ import { nanoid } from '@nangohq/utils'; import type { MessageRow } from '@nangohq/types'; +export const operationIdRegex = /([0-9]|[a-zA-Z0-9]{20})/; export interface FormatMessageData { account?: { id: number; name?: string }; user?: { id: number } | undefined; diff --git a/packages/logs/lib/models/messages.ts b/packages/logs/lib/models/messages.ts index 99ad7397bb..7d3deb6c69 100644 --- a/packages/logs/lib/models/messages.ts +++ b/packages/logs/lib/models/messages.ts @@ -1,5 +1,5 @@ import { client } from '../es/client.js'; -import type { MessageRow, OperationRow, SearchLogsState } from '@nangohq/types'; +import type { MessageRow, OperationRow, SearchOperationsState } from '@nangohq/types'; import { indexMessages } from '../es/schema.js'; import type { opensearchtypes } from '@opensearch-project/opensearch'; import { errors } from '@opensearch-project/opensearch'; @@ -30,7 +30,12 @@ export async function createMessage(row: MessageRow): Promise { /** * List operations */ -export async function listOperations(opts: { accountId: number; environmentId?: number; limit: number; states: SearchLogsState[] }): Promise { +export async function listOperations(opts: { + accountId: number; + environmentId?: number; + limit: number; + states?: SearchOperationsState[] | undefined; +}): Promise { const query: opensearchtypes.QueryDslQueryContainer = { bool: { must: [{ term: { accountId: opts.accountId } }], @@ -41,7 +46,7 @@ export async function listOperations(opts: { accountId: number; environmentId?: if (opts.environmentId) { (query.bool!.must as opensearchtypes.QueryDslQueryContainer[]).push({ term: { environmentId: opts.environmentId } }); } - if (opts.states.length > 1 || opts.states[0] !== 'all') { + if (opts.states && (opts.states.length > 1 || opts.states[0] !== 'all')) { (query.bool!.must as opensearchtypes.QueryDslQueryContainer[]).push({ bool: { should: opts.states.map((state) => { diff --git a/packages/server/lib/controllers/v1/logs/getOperation.ts b/packages/server/lib/controllers/v1/logs/getOperation.ts index 8787e1fe62..24a8d9a5b2 100644 --- a/packages/server/lib/controllers/v1/logs/getOperation.ts +++ b/packages/server/lib/controllers/v1/logs/getOperation.ts @@ -2,11 +2,11 @@ import { z } from 'zod'; import { asyncWrapper } from '../../../utils/asyncWrapper.js'; import { requireEmptyQuery, zodErrorToHTTP } from '../../../utils/validation.js'; import type { GetOperation } from '@nangohq/types'; -import { model, envs } from '@nangohq/logs'; +import { model, envs, operationIdRegex } from '@nangohq/logs'; const validation = z .object({ - operationId: z.string().regex(/([0-9]|[a-zA-Z0-9]{20})/) + operationId: z.string().regex(operationIdRegex) }) .strict(); diff --git a/packages/server/lib/controllers/v1/logs/searchLogs.integration.test.ts b/packages/server/lib/controllers/v1/logs/searchOperations.integration.test.ts similarity index 68% rename from packages/server/lib/controllers/v1/logs/searchLogs.integration.test.ts rename to packages/server/lib/controllers/v1/logs/searchOperations.integration.test.ts index e7d1182459..5bbd8d150f 100644 --- a/packages/server/lib/controllers/v1/logs/searchLogs.integration.test.ts +++ b/packages/server/lib/controllers/v1/logs/searchOperations.integration.test.ts @@ -1,7 +1,7 @@ import { logContextGetter, migrateMapping } from '@nangohq/logs'; import { multipleMigrations, seeders } from '@nangohq/shared'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import { runServer, shouldBeProtected, shouldRequireQueryEnv } from '../../../utils/tests.js'; +import { isSuccess, runServer, shouldBeProtected, shouldRequireQueryEnv } from '../../../utils/tests.js'; let api: Awaited>; describe('GET /logs', () => { @@ -16,21 +16,21 @@ describe('GET /logs', () => { }); it('should be protected', async () => { - const res = await api.fetch('/api/v1/logs/search', { method: 'POST', query: { env: 'dev' } }); + const res = await api.fetch('/api/v1/logs/operations', { method: 'POST', query: { env: 'dev' } }); shouldBeProtected(res); }); it('should enforce env query params', async () => { const { env } = await seeders.seedAccountEnvAndUser(); - const res = await api.fetch('/api/v1/logs/search', { method: 'POST', token: env.secret_key }); + const res = await api.fetch('/api/v1/logs/operations', { method: 'POST', token: env.secret_key }); shouldRequireQueryEnv(res); }); it('should validate body', async () => { const { env } = await seeders.seedAccountEnvAndUser(); - const res = await api.fetch('/api/v1/logs/search', { + const res = await api.fetch('/api/v1/logs/operations', { method: 'POST', query: { env: 'dev' }, token: env.secret_key, @@ -60,7 +60,7 @@ describe('GET /logs', () => { it('should search logs and get empty results', async () => { const { env } = await seeders.seedAccountEnvAndUser(); - const res = await api.fetch('/api/v1/logs/search', { + const res = await api.fetch('/api/v1/logs/operations', { method: 'POST', query: { env: 'dev' }, token: env.secret_key, @@ -75,16 +75,13 @@ describe('GET /logs', () => { }); it('should search logs and get one result', async () => { - const { env } = await seeders.seedAccountEnvAndUser(); + const { env, account } = await seeders.seedAccountEnvAndUser(); - const logCtx = await logContextGetter.create( - { message: 'test 1', operation: { type: 'auth' } }, - { account: { id: env.account_id }, environment: { id: env.id } } - ); + const logCtx = await logContextGetter.create({ message: 'test 1', operation: { type: 'auth' } }, { account, environment: env }); await logCtx.info('test info'); await logCtx.success(); - const res = await api.fetch('/api/v1/logs/search', { + const res = await api.fetch('/api/v1/logs/operations', { method: 'POST', query: { env: 'dev' }, token: env.secret_key, @@ -105,7 +102,7 @@ describe('GET /logs', () => { createdAt: expect.toBeIsoDate(), endedAt: expect.toBeIsoDate(), environmentId: env.id, - environmentName: null, + environmentName: 'dev', error: null, id: logCtx.id, jobId: null, @@ -141,7 +138,7 @@ describe('GET /logs', () => { await logCtx.info('test info'); await logCtx.success(); - const res = await api.fetch('/api/v1/logs/search', { + const res = await api.fetch('/api/v1/logs/operations', { method: 'POST', query: { env: 'dev' }, token: env2.env.secret_key, @@ -154,4 +151,37 @@ describe('GET /logs', () => { pagination: { total: 0 } }); }); + + describe('query params', () => { + it('should filter by operationId', async () => { + const { env } = await seeders.seedAccountEnvAndUser(); + + // first env + const logCtx = await logContextGetter.create( + { message: 'test 1', operation: { type: 'auth' } }, + { account: { id: env.account_id }, environment: { id: env.id } } + ); + await logCtx.info('test first env'); + await logCtx.success(); + + // other env + const env2 = await seeders.seedAccountEnvAndUser(); + const logCtx2 = await logContextGetter.create({ message: 'test 1', operation: { type: 'auth' } }, { account: env2.account, environment: env2.env }); + await logCtx2.info('test second env'); + await logCtx2.success(); + + const res = await api.fetch('/api/v1/logs/operations', { + method: 'POST', + query: { env: 'dev' }, + token: env.secret_key, + body: { limit: 10, operationId: logCtx2.id } + }); + + isSuccess(res.json); + expect(res.json.data).toMatchObject({ + id: logCtx2.id, + message: 'test second env' + }); + }); + }); }); diff --git a/packages/server/lib/controllers/v1/logs/searchLogs.ts b/packages/server/lib/controllers/v1/logs/searchOperations.ts similarity index 82% rename from packages/server/lib/controllers/v1/logs/searchLogs.ts rename to packages/server/lib/controllers/v1/logs/searchOperations.ts index 622306604c..239eda50c4 100644 --- a/packages/server/lib/controllers/v1/logs/searchLogs.ts +++ b/packages/server/lib/controllers/v1/logs/searchOperations.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { asyncWrapper } from '../../../utils/asyncWrapper.js'; import { requireEmptyQuery, zodErrorToHTTP } from '../../../utils/validation.js'; -import type { SearchLogs } from '@nangohq/types'; +import type { SearchOperations } from '@nangohq/types'; import { model, envs } from '@nangohq/logs'; const validation = z @@ -14,7 +14,7 @@ const validation = z }) .strict(); -export const searchLogs = asyncWrapper(async (req, res) => { +export const searchOperations = asyncWrapper(async (req, res) => { if (!envs.NANGO_LOGS_ENABLED) { res.status(404).send({ error: { code: 'feature_disabled' } }); return; @@ -35,8 +35,8 @@ export const searchLogs = asyncWrapper(async (req, res) => { } const env = res.locals['environment']; - const body: Required = val.data; - const rawOps = await model.listOperations({ accountId: env.account_id, environmentId: env.id, limit: body.limit, states: body.states }); + const body: SearchOperations['Body'] = val.data; + const rawOps = await model.listOperations({ accountId: env.account_id, environmentId: env.id, limit: body.limit!, states: body.states }); res.status(200).send({ data: rawOps.items, diff --git a/packages/server/lib/routes.ts b/packages/server/lib/routes.ts index 8d744f06dc..acfc5ad9f7 100644 --- a/packages/server/lib/routes.ts +++ b/packages/server/lib/routes.ts @@ -31,7 +31,7 @@ import type { Response, Request } from 'express'; import { isCloud, isEnterprise, AUTH_ENABLED, MANAGED_AUTH_ENABLED, isBasicAuthEnabled, isTest } from '@nangohq/utils'; import { errorManager } from '@nangohq/shared'; import tracer from 'dd-trace'; -import { searchLogs } from './controllers/v1/logs/searchLogs.js'; +import { searchOperations } from './controllers/v1/logs/searchOperations.js'; import { getOperation } from './controllers/v1/logs/getOperation.js'; export const app = express(); @@ -201,7 +201,8 @@ web.route('/api/v1/onboarding/deploy').post(webAuth, onboardingController.deploy web.route('/api/v1/onboarding/sync-status').post(webAuth, onboardingController.checkSyncCompletion.bind(onboardingController)); web.route('/api/v1/onboarding/action').post(webAuth, onboardingController.writeGithubIssue.bind(onboardingController)); -web.route('/api/v1/logs/search').post(webAuth, searchLogs); +web.route('/api/v1/logs/operations').post(webAuth, searchOperations); +// web.route('/api/v1/logs/messages').post(webAuth, searchOperations); web.route('/api/v1/logs/:operationId').get(webAuth, getOperation); // Hosted signin diff --git a/packages/server/lib/utils/tests.ts b/packages/server/lib/utils/tests.ts index c6ea4638c5..2a2a37cd39 100644 --- a/packages/server/lib/utils/tests.ts +++ b/packages/server/lib/utils/tests.ts @@ -3,7 +3,7 @@ import type { Server } from 'node:http'; import { createServer } from 'node:http'; import { expect } from 'vitest'; import type { APIEndpoints, APIEndpointsPicker, APIEndpointsPickerWithPath, ApiError } from '@nangohq/types'; -import { getServerPort } from '@nangohq/shared'; +import getPort from 'get-port'; import { app } from '../routes.js'; @@ -101,8 +101,8 @@ export function shouldRequireQueryEnv({ res, json }: { res: Response; json: any */ export async function runServer(): Promise<{ server: Server; url: string; fetch: ReturnType }> { const server = createServer(app); + const port = await getPort(); return new Promise((resolve) => { - const port = getServerPort(); server.listen(port, () => { const url = `http://localhost:${port}`; resolve({ server, url, fetch: apiFetch(url) }); diff --git a/packages/server/package.json b/packages/server/package.json index dd3a38ae1b..9a869d086a 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -79,6 +79,7 @@ "@types/simple-oauth2": "^4.1.1", "@types/uuid": "^8.3.4", "@types/ws": "^8.5.4", + "get-port": "7.1.0", "nodemon": "^3.0.1", "typescript": "^5.3.3", "vitest": "0.33.0" diff --git a/packages/types/lib/api.endpoints.ts b/packages/types/lib/api.endpoints.ts index 3e9df30468..7f061b275b 100644 --- a/packages/types/lib/api.endpoints.ts +++ b/packages/types/lib/api.endpoints.ts @@ -1,8 +1,8 @@ import type { EndpointMethod } from './api'; -import type { GetOperation, SearchLogs } from './logs/api'; +import type { GetOperation, SearchOperations } from './logs/api'; import type { GetOnboardingStatus } from './onboarding/api'; -export type APIEndpoints = SearchLogs | GetOperation | GetOnboardingStatus; +export type APIEndpoints = SearchOperations | GetOperation | GetOnboardingStatus; /** * Automatically narrow endpoints type with Method + Path diff --git a/packages/types/lib/logs/api.ts b/packages/types/lib/logs/api.ts index 458f615289..8e3c18f9c9 100644 --- a/packages/types/lib/logs/api.ts +++ b/packages/types/lib/logs/api.ts @@ -1,20 +1,20 @@ import type { Endpoint } from '../api'; import type { MessageState, OperationRow } from './messages'; -export type SearchLogs = Endpoint<{ +export type SearchOperations = Endpoint<{ Method: 'POST'; - Path: '/api/v1/logs/search'; + Path: '/api/v1/logs/operations'; Querystring: { env: string }; - Body: { limit?: number; states?: SearchLogsState[] }; + Body: { limit?: number; states?: SearchOperationsState[]; operationId?: string | undefined }; Success: { data: OperationRow[]; pagination: { total: number }; }; }>; -export type SearchLogsState = 'all' | MessageState; +export type SearchOperationsState = 'all' | MessageState; -export type SearchLogsData = SearchLogs['Success']['data'][0]; +export type SearchOperationsData = SearchOperations['Success']['data'][0]; export type GetOperation = Endpoint<{ Method: 'GET'; diff --git a/packages/webapp/src/components/ui/Table.tsx b/packages/webapp/src/components/ui/Table.tsx index e3572b3e5e..74b74d1e0e 100644 --- a/packages/webapp/src/components/ui/Table.tsx +++ b/packages/webapp/src/components/ui/Table.tsx @@ -2,7 +2,7 @@ import { forwardRef } from 'react'; import { cn } from '../../utils/utils'; const Table = forwardRef>(({ className, ...props }, ref) => ( -
+
)); diff --git a/packages/webapp/src/hooks/useLogs.tsx b/packages/webapp/src/hooks/useLogs.tsx index acbffb9398..6df67c8fa5 100644 --- a/packages/webapp/src/hooks/useLogs.tsx +++ b/packages/webapp/src/hooks/useLogs.tsx @@ -1,12 +1,12 @@ -import type { GetOperation, SearchLogs } from '@nangohq/types'; +import type { GetOperation, SearchOperations } from '@nangohq/types'; import { useEffect, useState } from 'react'; import useSWR from 'swr'; import { swrFetcher } from '../utils/api'; -export function useSearchLogs(env: string, body: SearchLogs['Body']) { +export function useSearchOperations(env: string, body: SearchOperations['Body']) { const [loading, setLoading] = useState(false); - const [data, setData] = useState(); - const [error, setError] = useState(); + const [data, setData] = useState(); + const [error, setError] = useState(); async function fetchData() { setLoading(true); @@ -18,12 +18,12 @@ export function useSearchLogs(env: string, body: SearchLogs['Body']) { }); if (res.status !== 200) { setData(undefined); - setError((await res.json()) as SearchLogs['Errors']); + setError((await res.json()) as SearchOperations['Errors']); return; } setError(undefined); - setData((await res.json()) as SearchLogs['Success']); + setData((await res.json()) as SearchOperations['Success']); } catch (err) { console.log(err); setData(undefined); diff --git a/packages/webapp/src/pages/Logs/Search.tsx b/packages/webapp/src/pages/Logs/Search.tsx index 2ff2404521..b098d14cc8 100644 --- a/packages/webapp/src/pages/Logs/Search.tsx +++ b/packages/webapp/src/pages/Logs/Search.tsx @@ -3,14 +3,14 @@ import DashboardLayout from '../../layout/DashboardLayout'; import { useStore } from '../../store'; import Info from '../../components/ui/Info'; import { Loading } from '@geist-ui/core'; -import { useSearchLogs } from '../../hooks/useLogs'; +import { useSearchOperations } from '../../hooks/useLogs'; import * as Table from '../../components/ui/Table'; import { getCoreRowModel, useReactTable, flexRender } from '@tanstack/react-table'; import { MultiSelect } from './components/MultiSelect'; import { columns, statusDefaultOptions, statusOptions } from './constants'; import { useEffect, useState } from 'react'; -import type { SearchLogsState } from '@nangohq/types'; +import type { SearchOperationsState } from '@nangohq/types'; import Spinner from '../../components/ui/Spinner'; import { OperationRow } from './components/OperationRow'; @@ -18,9 +18,9 @@ export const LogsSearch: React.FC = () => { const env = useStore((state) => state.env); // Data fetch - const [states, setStates] = useState(statusDefaultOptions); + const [states, setStates] = useState(statusDefaultOptions); const [hasLogs, setHasLogs] = useState(false); - const { data, error, loading } = useSearchLogs(env, { limit: 20, states }); + const { data, error, loading } = useSearchOperations(env, { limit: 20, states }); const table = useReactTable({ data: data ? data.data : [], @@ -78,7 +78,7 @@ export const LogsSearch: React.FC = () => { - + {table.getHeaderGroups().map((headerGroup) => ( diff --git a/packages/webapp/src/pages/Logs/Show.tsx b/packages/webapp/src/pages/Logs/Show.tsx index 272ce72348..fc746ca736 100644 --- a/packages/webapp/src/pages/Logs/Show.tsx +++ b/packages/webapp/src/pages/Logs/Show.tsx @@ -8,6 +8,7 @@ import { StatusTag } from './components/StatusTag'; import { elapsedTime } from '../../utils/utils'; import { Link } from 'react-router-dom'; import { ExternalLinkIcon } from '@radix-ui/react-icons'; +import { SearchInOperation } from './components/SearchInOperation'; export const Show: React.FC<{ operationId: string }> = ({ operationId }) => { const env = useStore((state) => state.env); @@ -17,11 +18,11 @@ export const Show: React.FC<{ operationId: string }> = ({ operationId }) => { if (!operation) { return ''; } - if (!operation.endedAt) { + if (!operation.endedAt || !operation.startedAt) { return 'n/a'; } - return elapsedTime(new Date(operation.startedAt!), new Date(operation.endedAt)); + return elapsedTime(new Date(operation.startedAt), new Date(operation.endedAt)); }, [operation]); if (loading) { @@ -29,7 +30,11 @@ export const Show: React.FC<{ operationId: string }> = ({ operationId }) => { } if (error || !operation) { - return An error occurred; + return ( + + An error occurred + + ); } return ( @@ -87,9 +92,7 @@ export const Show: React.FC<{ operationId: string }> = ({ operationId }) => {

Payload

{!operation.meta &&
No payload...
} -
-

Logs

-
+ ); }; diff --git a/packages/webapp/src/pages/Logs/components/MultiSelect.tsx b/packages/webapp/src/pages/Logs/components/MultiSelect.tsx index 5fcbf748db..169cbfcb31 100644 --- a/packages/webapp/src/pages/Logs/components/MultiSelect.tsx +++ b/packages/webapp/src/pages/Logs/components/MultiSelect.tsx @@ -1,20 +1,20 @@ -import type { SearchLogsState } from '@nangohq/types'; +import type { SearchOperationsState } from '@nangohq/types'; import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger } from '../../../components/ui/DropdownMenu'; import Button from '../../../components/ui/button/Button'; import { useState } from 'react'; export interface MultiSelectArgs { label: string; - options: { name: string; value: SearchLogsState }[]; - selected: SearchLogsState[]; - defaultSelect: SearchLogsState[]; + options: { name: string; value: SearchOperationsState }[]; + selected: SearchOperationsState[]; + defaultSelect: SearchOperationsState[]; all?: boolean; - onChange: (selected: SearchLogsState[]) => void; + onChange: (selected: SearchOperationsState[]) => void; } export const MultiSelect: React.FC = ({ label, options, selected, defaultSelect, all, onChange }) => { const [open, setOpen] = useState(false); - const select = (val: SearchLogsState, checked: boolean) => { + const select = (val: SearchOperationsState, checked: boolean) => { if (all && val === 'all') { onChange(['all']); return; diff --git a/packages/webapp/src/pages/Logs/components/OperationRow.tsx b/packages/webapp/src/pages/Logs/components/OperationRow.tsx index 4743715e15..5c9b66717f 100644 --- a/packages/webapp/src/pages/Logs/components/OperationRow.tsx +++ b/packages/webapp/src/pages/Logs/components/OperationRow.tsx @@ -1,6 +1,6 @@ import type { Row } from '@tanstack/react-table'; import { flexRender } from '@tanstack/react-table'; -import type { SearchLogsData } from '@nangohq/types'; +import type { SearchOperationsData } from '@nangohq/types'; import { Drawer, DrawerContent, DrawerTrigger, DrawerClose } from '../../../components/ui/Drawer'; import * as Table from '../../../components/ui/Table'; @@ -8,7 +8,7 @@ import { Show } from '../Show'; import { Cross1Icon } from '@radix-ui/react-icons'; const drawerWidth = '1034px'; -export const OperationRow: React.FC<{ row: Row }> = ({ row }) => { +export const OperationRow: React.FC<{ row: Row }> = ({ row }) => { return ( diff --git a/packages/webapp/src/pages/Logs/components/OperationTag.tsx b/packages/webapp/src/pages/Logs/components/OperationTag.tsx index deba48ee4d..38ac6c16e2 100644 --- a/packages/webapp/src/pages/Logs/components/OperationTag.tsx +++ b/packages/webapp/src/pages/Logs/components/OperationTag.tsx @@ -1,8 +1,8 @@ // import { ChevronRight } from '@geist-ui/icons'; -import type { SearchLogsData } from '@nangohq/types'; +import type { SearchOperationsData } from '@nangohq/types'; import { cn } from '../../../utils/utils'; -export const OperationTag: React.FC<{ operation: Exclude; highlight?: boolean }> = ({ operation, highlight }) => { +export const OperationTag: React.FC<{ operation: Exclude; highlight?: boolean }> = ({ operation, highlight }) => { return (
[] = [ + { + accessorKey: 'createdAt', + header: 'Timestamp', + size: 120, + cell: ({ row }) => { + return formatDateToInternationalFormat(row.original.createdAt); + } + }, + { + accessorKey: 'state', + header: 'Status', + size: 100, + cell: ({ row }) => { + return ; + } + }, + { + accessorKey: 'operation', + header: 'Type', + size: 100, + cell: ({ row }) => { + return row.original.type; + } + }, + { + accessorKey: 'message', + header: 'Additional Info', + cell: ({ row }) => { + return
{row.original.message}
; + } + } +]; export const SearchInOperation: React.FC<{ operationId: string }> = ({ operationId }) => { + const env = useStore((state) => state.env); + + const { data, error, loading } = useSearchOperations(env, { limit: 20, operationId }); + + const table = useReactTable({ + data: data ? data.data : [], + columns, + getCoreRowModel: getCoreRowModel() + }); + return (
+

Logs {loading && }

-
+
+ {error && ( + + An error occurred + + )} + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ) + ) : ( + + + No results. + + + )} + + +
); }; diff --git a/packages/webapp/src/pages/Logs/components/StatusTag.tsx b/packages/webapp/src/pages/Logs/components/StatusTag.tsx index 26f4c959f5..1603d18259 100644 --- a/packages/webapp/src/pages/Logs/components/StatusTag.tsx +++ b/packages/webapp/src/pages/Logs/components/StatusTag.tsx @@ -1,6 +1,6 @@ -import type { SearchLogsData } from '@nangohq/types'; +import type { SearchOperationsData } from '@nangohq/types'; -export const StatusTag: React.FC<{ state: SearchLogsData['state'] }> = ({ state }) => { +export const StatusTag: React.FC<{ state: SearchOperationsData['state'] }> = ({ state }) => { if (state === 'success') { return (
diff --git a/packages/webapp/src/pages/Logs/constants.tsx b/packages/webapp/src/pages/Logs/constants.tsx index c87b8d6f51..404f0c59a1 100644 --- a/packages/webapp/src/pages/Logs/constants.tsx +++ b/packages/webapp/src/pages/Logs/constants.tsx @@ -1,16 +1,16 @@ import type { ColumnDef } from '@tanstack/react-table'; -import type { SearchLogsData, SearchLogsState } from '@nangohq/types'; +import type { SearchOperationsData, SearchOperationsState } from '@nangohq/types'; import { formatDateToInternationalFormat } from '../../utils/utils'; import { StatusTag } from './components/StatusTag'; import { OperationTag } from './components/OperationTag'; import type { MultiSelectArgs } from './components/MultiSelect'; import { ChevronRight } from '@geist-ui/icons'; -export const columns: ColumnDef[] = [ +export const columns: ColumnDef[] = [ { accessorKey: 'createdAt', header: 'Timestamp', - size: 150, + size: 120, cell: ({ row }) => { return formatDateToInternationalFormat(row.original.createdAt); } @@ -26,9 +26,9 @@ export const columns: ColumnDef[] = [ { accessorKey: 'operation', header: 'Type', - size: 200, + size: 100, cell: ({ row }) => { - return ; + return ; } }, { @@ -36,7 +36,7 @@ export const columns: ColumnDef[] = [ header: 'Integration', size: 200, cell: ({ row }) => { - return row.original.configName; + return
{row.original.configName}
; } }, { @@ -44,7 +44,7 @@ export const columns: ColumnDef[] = [ header: 'Script', size: 200, cell: ({ row }) => { - return row.original.syncName; + return
{row.original.syncName}
; } }, { @@ -52,16 +52,16 @@ export const columns: ColumnDef[] = [ header: 'Connection', size: 200, cell: ({ row }) => { - return row.original.connectionName; + return
{row.original.connectionName}
; } }, { accessorKey: 'id', header: '', - size: 10, + size: 20, cell: () => { return ( -
+
); @@ -69,7 +69,7 @@ export const columns: ColumnDef[] = [ } ]; -export const statusDefaultOptions: SearchLogsState[] = ['all']; +export const statusDefaultOptions: SearchOperationsState[] = ['all']; export const statusOptions: MultiSelectArgs['options'] = [ { name: 'All', From 165605d12a6b4c0db72f5da50bce2111e291dedc Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Wed, 15 May 2024 10:48:28 +0200 Subject: [PATCH 03/14] more --- packages/logs/lib/models/messages.ts | 22 ++- .../logs/searchMessages.integration.test.ts | 162 ++++++++++++++++++ .../lib/controllers/v1/logs/searchMessages.ts | 63 +++++++ .../logs/searchOperations.integration.test.ts | 36 +--- packages/server/lib/routes.ts | 3 +- packages/types/lib/api.endpoints.ts | 4 +- packages/types/lib/logs/api.ts | 15 +- packages/webapp/src/hooks/useLogs.tsx | 47 ++++- packages/webapp/src/pages/Logs/Show.tsx | 18 +- .../Logs/components/SearchInOperation.tsx | 4 +- packages/webapp/src/pages/Logs/constants.tsx | 2 +- 11 files changed, 319 insertions(+), 57 deletions(-) create mode 100644 packages/server/lib/controllers/v1/logs/searchMessages.integration.test.ts create mode 100644 packages/server/lib/controllers/v1/logs/searchMessages.ts diff --git a/packages/logs/lib/models/messages.ts b/packages/logs/lib/models/messages.ts index 7d3deb6c69..dfe1296e88 100644 --- a/packages/logs/lib/models/messages.ts +++ b/packages/logs/lib/models/messages.ts @@ -139,10 +139,27 @@ export async function setTimeouted(opts: Pick): Promise /** * List messages */ -export async function listMessages(opts: { parentId: MessageRow['parentId']; limit: number }): Promise { +export async function listMessages(opts: { parentId: string; limit: number; states?: SearchOperationsState[] | undefined }): Promise { + const query: opensearchtypes.QueryDslQueryContainer = { + bool: { + must: [{ term: { parentId: opts.parentId } }], + should: [] + } + }; + + if (opts.states && (opts.states.length > 1 || opts.states[0] !== 'all')) { + (query.bool!.must as opensearchtypes.QueryDslQueryContainer[]).push({ + bool: { + should: opts.states.map((state) => { + return { term: { state } }; + }) + } + }); + } + const res = await client.search<{ hits: { total: number; hits: { _source: MessageRow }[] } }>({ index: indexMessages.index, - size: 5000, + size: opts.limit, sort: ['createdAt:desc', '_score'], track_total_hits: true, body: { @@ -153,7 +170,6 @@ export async function listMessages(opts: { parentId: MessageRow['parentId']; lim } } }); - const hits = res.body.hits; return { diff --git a/packages/server/lib/controllers/v1/logs/searchMessages.integration.test.ts b/packages/server/lib/controllers/v1/logs/searchMessages.integration.test.ts new file mode 100644 index 0000000000..e658c0c7d7 --- /dev/null +++ b/packages/server/lib/controllers/v1/logs/searchMessages.integration.test.ts @@ -0,0 +1,162 @@ +import { logContextGetter, migrateMapping } from '@nangohq/logs'; +import { multipleMigrations, seeders } from '@nangohq/shared'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { isError, isSuccess, runServer, shouldBeProtected, shouldRequireQueryEnv } from '../../../utils/tests.js'; + +let api: Awaited>; +describe('GET /logs', () => { + beforeAll(async () => { + await multipleMigrations(); + await migrateMapping(); + + api = await runServer(); + }); + afterAll(() => { + api.server.close(); + }); + + it('should be protected', async () => { + const res = await api.fetch('/api/v1/logs/messages', { method: 'POST', query: { env: 'dev' }, body: { operationId: '1' } }); + + shouldBeProtected(res); + }); + + it('should enforce env query params', async () => { + const { env } = await seeders.seedAccountEnvAndUser(); + // @ts-expect-error missing query on purpose + const res = await api.fetch('/api/v1/logs/messages', { method: 'POST', token: env.secret_key, body: { operationId: '1' } }); + + shouldRequireQueryEnv(res); + }); + + it('should validate body', async () => { + const { env } = await seeders.seedAccountEnvAndUser(); + const res = await api.fetch('/api/v1/logs/messages', { + method: 'POST', + query: { env: 'dev' }, + token: env.secret_key, + // @ts-expect-error on purpose + body: { limit: 'a', foo: 'bar' } + }); + + expect(res.json).toStrictEqual({ + error: { + code: 'invalid_body', + errors: [ + { + code: 'invalid_type', + message: 'Required', + path: ['operationId'] + }, + { + code: 'invalid_type', + message: 'Expected number, received string', + path: ['limit'] + }, + { + code: 'unrecognized_keys', + message: "Unrecognized key(s) in object: 'foo'", + path: [] + } + ] + } + }); + expect(res.res.status).toBe(400); + }); + + it('should search messages and get empty results', async () => { + const { account, env } = await seeders.seedAccountEnvAndUser(); + + const logCtx = await logContextGetter.create({ message: 'test 1', operation: { type: 'auth' } }, { account, environment: env }); + await logCtx.success(); + + const res = await api.fetch('/api/v1/logs/messages', { + method: 'POST', + query: { env: 'dev' }, + token: env.secret_key, + body: { operationId: logCtx.id, limit: 10 } + }); + + isSuccess(res.json); + expect(res.res.status).toBe(200); + expect(res.json).toStrictEqual({ + data: [], + pagination: { total: 0 } + }); + }); + + it('should search messages and get one result', async () => { + const { env, account } = await seeders.seedAccountEnvAndUser(); + + const logCtx = await logContextGetter.create({ message: 'test 1', operation: { type: 'auth' } }, { account, environment: env }); + await logCtx.info('test info'); + await logCtx.success(); + + const res = await api.fetch('/api/v1/logs/messages', { + method: 'POST', + query: { env: 'dev' }, + token: env.secret_key, + body: { operationId: logCtx.id, limit: 10 } + }); + + isSuccess(res.json); + expect(res.res.status).toBe(200); + expect(res.json).toStrictEqual({ + data: [ + { + accountId: null, + accountName: null, + code: null, + configId: null, + configName: null, + connectionId: null, + connectionName: null, + createdAt: expect.toBeIsoDate(), + endedAt: null, + environmentId: null, + environmentName: null, + error: null, + id: expect.any(String), + jobId: null, + level: 'info', + message: 'test info', + meta: null, + operation: null, + parentId: logCtx.id, + request: null, + response: null, + source: 'internal', + startedAt: null, + state: 'waiting', + syncId: null, + syncName: null, + title: null, + type: 'log', + updatedAt: expect.toBeIsoDate(), + userId: null + } + ], + pagination: { total: 1 } + }); + }); + + it('should search messages and not return results from an other account', async () => { + const { account, env } = await seeders.seedAccountEnvAndUser(); + const env2 = await seeders.seedAccountEnvAndUser(); + + const logCtx = await logContextGetter.create({ message: 'test 1', operation: { type: 'auth' } }, { account, environment: env }); + await logCtx.info('test info'); + await logCtx.success(); + + const res = await api.fetch('/api/v1/logs/messages', { + method: 'POST', + query: { env: 'dev' }, + token: env2.env.secret_key, + body: { operationId: logCtx.id, limit: 10 } + }); + + isError(res.json); + expect(res.res.status).toBe(404); + expect(res.json).toStrictEqual({ error: { code: 'not_found' } }); + }); +}); diff --git a/packages/server/lib/controllers/v1/logs/searchMessages.ts b/packages/server/lib/controllers/v1/logs/searchMessages.ts new file mode 100644 index 0000000000..757cf39c6a --- /dev/null +++ b/packages/server/lib/controllers/v1/logs/searchMessages.ts @@ -0,0 +1,63 @@ +import { z } from 'zod'; +import { asyncWrapper } from '../../../utils/asyncWrapper.js'; +import { requireEmptyQuery, zodErrorToHTTP } from '../../../utils/validation.js'; +import type { SearchMessages } from '@nangohq/types'; +import { model, envs, operationIdRegex } from '@nangohq/logs'; + +const validation = z + .object({ + operationId: z.string().regex(operationIdRegex), + limit: z.number().optional().default(100), + states: z + .array(z.enum(['all', 'waiting', 'running', 'success', 'failed', 'timeout', 'cancelled'])) + .optional() + .default(['all']) + }) + .strict(); + +export const searchMessages = asyncWrapper(async (req, res) => { + if (!envs.NANGO_LOGS_ENABLED) { + res.status(404).send({ error: { code: 'feature_disabled' } }); + return; + } + + const emptyQuery = requireEmptyQuery(req, { withEnv: true }); + if (emptyQuery) { + res.status(400).send({ error: { code: 'invalid_query_params', errors: zodErrorToHTTP(emptyQuery.error) } }); + return; + } + + const val = validation.safeParse(req.body); + if (!val.success) { + res.status(400).send({ + error: { code: 'invalid_body', errors: zodErrorToHTTP(val.error) } + }); + return; + } + + const { environment, account } = res.locals; + + // Manually ensure that `operationId` belongs to the account for now + // Because not all the logs have accountId/environmentId + try { + const operation = await model.getOperation({ id: val.data.operationId }); + if (operation.accountId !== account.id || operation.environmentId !== environment.id) { + res.status(404).send({ error: { code: 'not_found' } }); + return; + } + } catch (err) { + if (err instanceof model.ResponseError && err.statusCode === 404) { + res.status(404).send({ error: { code: 'not_found' } }); + return; + } + throw err; + } + + const body: SearchMessages['Body'] = val.data; + const rawOps = await model.listMessages({ parentId: body.operationId, limit: body.limit!, states: body.states }); + + res.status(200).send({ + data: rawOps.items, + pagination: { total: rawOps.count } + }); +}); diff --git a/packages/server/lib/controllers/v1/logs/searchOperations.integration.test.ts b/packages/server/lib/controllers/v1/logs/searchOperations.integration.test.ts index 8128fff3e4..b323d75844 100644 --- a/packages/server/lib/controllers/v1/logs/searchOperations.integration.test.ts +++ b/packages/server/lib/controllers/v1/logs/searchOperations.integration.test.ts @@ -69,6 +69,7 @@ describe('GET /logs', () => { body: { limit: 10 } }); + isSuccess(res.json); expect(res.res.status).toBe(200); expect(res.json).toStrictEqual({ data: [], @@ -90,6 +91,7 @@ describe('GET /logs', () => { body: { limit: 10 } }); + isSuccess(res.json); expect(res.res.status).toBe(200); expect(res.json).toStrictEqual({ data: [ @@ -147,43 +149,11 @@ describe('GET /logs', () => { body: { limit: 10 } }); + isSuccess(res.json); expect(res.res.status).toBe(200); expect(res.json).toStrictEqual({ data: [], pagination: { total: 0 } }); }); - - describe('query params', () => { - it('should filter by operationId', async () => { - const { env } = await seeders.seedAccountEnvAndUser(); - - // first env - const logCtx = await logContextGetter.create( - { message: 'test 1', operation: { type: 'auth' } }, - { account: { id: env.account_id }, environment: { id: env.id } } - ); - await logCtx.info('test first env'); - await logCtx.success(); - - // other env - const env2 = await seeders.seedAccountEnvAndUser(); - const logCtx2 = await logContextGetter.create({ message: 'test 1', operation: { type: 'auth' } }, { account: env2.account, environment: env2.env }); - await logCtx2.info('test second env'); - await logCtx2.success(); - - const res = await api.fetch('/api/v1/logs/operations', { - method: 'POST', - query: { env: 'dev' }, - token: env.secret_key, - body: { limit: 10, operationId: logCtx2.id } - }); - - isSuccess(res.json); - expect(res.json.data).toMatchObject({ - id: logCtx2.id, - message: 'test second env' - }); - }); - }); }); diff --git a/packages/server/lib/routes.ts b/packages/server/lib/routes.ts index a87be578a4..1c25eefef4 100644 --- a/packages/server/lib/routes.ts +++ b/packages/server/lib/routes.ts @@ -33,6 +33,7 @@ import { errorManager } from '@nangohq/shared'; import tracer from 'dd-trace'; import { searchOperations } from './controllers/v1/logs/searchOperations.js'; import { getOperation } from './controllers/v1/logs/getOperation.js'; +import { searchMessages } from './controllers/v1/logs/searchMessages.js'; export const app = express(); @@ -203,7 +204,7 @@ web.route('/api/v1/onboarding/sync-status').post(webAuth, onboardingController.c web.route('/api/v1/onboarding/action').post(webAuth, onboardingController.writeGithubIssue.bind(onboardingController)); web.route('/api/v1/logs/operations').post(webAuth, searchOperations); -// web.route('/api/v1/logs/messages').post(webAuth, searchOperations); +web.route('/api/v1/logs/messages').post(webAuth, searchMessages); web.route('/api/v1/logs/operations/:operationId').get(webAuth, getOperation); // Hosted signin diff --git a/packages/types/lib/api.endpoints.ts b/packages/types/lib/api.endpoints.ts index 7f061b275b..4e25dcdc46 100644 --- a/packages/types/lib/api.endpoints.ts +++ b/packages/types/lib/api.endpoints.ts @@ -1,8 +1,8 @@ import type { EndpointMethod } from './api'; -import type { GetOperation, SearchOperations } from './logs/api'; +import type { GetOperation, SearchMessages, SearchOperations } from './logs/api'; import type { GetOnboardingStatus } from './onboarding/api'; -export type APIEndpoints = SearchOperations | GetOperation | GetOnboardingStatus; +export type APIEndpoints = SearchOperations | GetOperation | SearchMessages | GetOnboardingStatus; /** * Automatically narrow endpoints type with Method + Path diff --git a/packages/types/lib/logs/api.ts b/packages/types/lib/logs/api.ts index b752d078b9..0bff0c670e 100644 --- a/packages/types/lib/logs/api.ts +++ b/packages/types/lib/logs/api.ts @@ -1,5 +1,5 @@ import type { Endpoint } from '../api'; -import type { MessageState, OperationRow } from './messages'; +import type { MessageRow, MessageState, OperationRow } from './messages'; export type SearchOperations = Endpoint<{ Method: 'POST'; @@ -11,9 +11,7 @@ export type SearchOperations = Endpoint<{ pagination: { total: number }; }; }>; - export type SearchOperationsState = 'all' | MessageState; - export type SearchOperationsData = SearchOperations['Success']['data'][0]; export type GetOperation = Endpoint<{ @@ -25,3 +23,14 @@ export type GetOperation = Endpoint<{ data: OperationRow; }; }>; + +export type SearchMessages = Endpoint<{ + Method: 'POST'; + Path: '/api/v1/logs/messages'; + Querystring: { env: string }; + Body: { operationId: string; limit?: number; states?: SearchOperationsState[] }; + Success: { + data: MessageRow[]; + pagination: { total: number }; + }; +}>; diff --git a/packages/webapp/src/hooks/useLogs.tsx b/packages/webapp/src/hooks/useLogs.tsx index 6df67c8fa5..cdc075ea9c 100644 --- a/packages/webapp/src/hooks/useLogs.tsx +++ b/packages/webapp/src/hooks/useLogs.tsx @@ -1,4 +1,4 @@ -import type { GetOperation, SearchOperations } from '@nangohq/types'; +import type { GetOperation, SearchMessages, SearchOperations } from '@nangohq/types'; import { useEffect, useState } from 'react'; import useSWR from 'swr'; import { swrFetcher } from '../utils/api'; @@ -11,7 +11,7 @@ export function useSearchOperations(env: string, body: SearchOperations['Body']) async function fetchData() { setLoading(true); try { - const res = await fetch(`/api/v1/logs/search?env=${env}`, { + const res = await fetch(`/api/v1/logs/operations?env=${env}`, { method: 'POST', body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' } @@ -42,8 +42,8 @@ export function useSearchOperations(env: string, body: SearchOperations['Body']) return { data, error, loading }; } -export function useOperation(env: string, params: GetOperation['Params']) { - const { data, error } = useSWR(`/api/v1/logs/${params.operationId}?env=${env}`, swrFetcher); +export function useGetOperation(env: string, params: GetOperation['Params']) { + const { data, error } = useSWR(`/api/v1/logs/operations/${params.operationId}?env=${env}`, swrFetcher); const loading = !data && !error; @@ -53,3 +53,42 @@ export function useOperation(env: string, params: GetOperation['Params']) { operation: data?.data }; } + +export function useSearchMessages(env: string, body: SearchMessages['Body']) { + const [loading, setLoading] = useState(false); + const [data, setData] = useState(); + const [error, setError] = useState(); + + async function fetchData() { + setLoading(true); + try { + const res = await fetch(`/api/v1/logs/messages?env=${env}`, { + method: 'POST', + body: JSON.stringify(body), + headers: { 'Content-Type': 'application/json' } + }); + if (res.status !== 200) { + setData(undefined); + setError((await res.json()) as SearchMessages['Errors']); + return; + } + + setError(undefined); + setData((await res.json()) as SearchMessages['Success']); + } catch (err) { + console.log(err); + setData(undefined); + setError(err as any); + } finally { + setLoading(false); + } + } + + useEffect(() => { + if (!loading) { + void fetchData(); + } + }, [env, body.operationId, body.limit, body.states]); + + return { data, error, loading }; +} diff --git a/packages/webapp/src/pages/Logs/Show.tsx b/packages/webapp/src/pages/Logs/Show.tsx index fc746ca736..66577d250c 100644 --- a/packages/webapp/src/pages/Logs/Show.tsx +++ b/packages/webapp/src/pages/Logs/Show.tsx @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import Info from '../../components/ui/Info'; import Spinner from '../../components/ui/Spinner'; -import { useOperation } from '../../hooks/useLogs'; +import { useGetOperation } from '../../hooks/useLogs'; import { useStore } from '../../store'; import { OperationTag } from './components/OperationTag'; import { StatusTag } from './components/StatusTag'; @@ -12,7 +12,7 @@ import { SearchInOperation } from './components/SearchInOperation'; export const Show: React.FC<{ operationId: string }> = ({ operationId }) => { const env = useStore((state) => state.env); - const { operation, loading, error } = useOperation(env, { operationId }); + const { operation, loading, error } = useGetOperation(env, { operationId }); const duration = useMemo(() => { if (!operation) { @@ -31,16 +31,18 @@ export const Show: React.FC<{ operationId: string }> = ({ operationId }) => { if (error || !operation) { return ( - - An error occurred - +
+ + An error occurred + +
); } return ( -
+

Operation Details

-
+
Timestamp
{operation.startedAt}
@@ -88,7 +90,7 @@ export const Show: React.FC<{ operationId: string }> = ({ operationId }) => {
-
+

Payload

{!operation.meta &&
No payload...
}
diff --git a/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx index 6104043c18..7748b6a045 100644 --- a/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx +++ b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx @@ -1,7 +1,7 @@ import type { ColumnDef } from '@tanstack/react-table'; import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; import { Input } from '../../../components/ui/input/Input'; -import { useSearchOperations } from '../../../hooks/useLogs'; +import { useSearchMessages } from '../../../hooks/useLogs'; import type { SearchOperationsData } from '@nangohq/types'; import { formatDateToInternationalFormat } from '../../../utils/utils'; import { StatusTag } from './StatusTag'; @@ -48,7 +48,7 @@ export const columns: ColumnDef[] = [ export const SearchInOperation: React.FC<{ operationId: string }> = ({ operationId }) => { const env = useStore((state) => state.env); - const { data, error, loading } = useSearchOperations(env, { limit: 20, operationId }); + const { data, error, loading } = useSearchMessages(env, { limit: 20, operationId }); const table = useReactTable({ data: data ? data.data : [], diff --git a/packages/webapp/src/pages/Logs/constants.tsx b/packages/webapp/src/pages/Logs/constants.tsx index 404f0c59a1..9989bed742 100644 --- a/packages/webapp/src/pages/Logs/constants.tsx +++ b/packages/webapp/src/pages/Logs/constants.tsx @@ -28,7 +28,7 @@ export const columns: ColumnDef[] = [ header: 'Type', size: 100, cell: ({ row }) => { - return ; + return ; } }, { From 3b85df3594a5add3934dc5d104ed99883c79ad1c Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Wed, 15 May 2024 12:13:52 +0200 Subject: [PATCH 04/14] more --- packages/types/lib/logs/api.ts | 1 + .../webapp/src/components/ui/Skeleton.tsx | 7 ++++ .../webapp/src/components/ui/input/Input.tsx | 4 +-- packages/webapp/src/pages/Logs/Search.tsx | 6 +++- packages/webapp/src/pages/Logs/Show.tsx | 18 ++++++++-- .../src/pages/Logs/components/LevelTag.tsx | 31 +++++++++++++++++ .../src/pages/Logs/components/MessageRow.tsx | 33 +++++++++++++++++++ .../src/pages/Logs/components/MessageTag.tsx | 19 +++++++++++ .../Logs/components/SearchInOperation.tsx | 33 +++++++++++-------- .../src/pages/Logs/components/StatusTag.tsx | 2 +- packages/webapp/tailwind.config.js | 1 - 11 files changed, 134 insertions(+), 21 deletions(-) create mode 100644 packages/webapp/src/components/ui/Skeleton.tsx create mode 100644 packages/webapp/src/pages/Logs/components/LevelTag.tsx create mode 100644 packages/webapp/src/pages/Logs/components/MessageRow.tsx create mode 100644 packages/webapp/src/pages/Logs/components/MessageTag.tsx diff --git a/packages/types/lib/logs/api.ts b/packages/types/lib/logs/api.ts index 0bff0c670e..4a6b9fa0fe 100644 --- a/packages/types/lib/logs/api.ts +++ b/packages/types/lib/logs/api.ts @@ -34,3 +34,4 @@ export type SearchMessages = Endpoint<{ pagination: { total: number }; }; }>; +export type SearchMessagesData = SearchMessages['Success']['data'][0]; diff --git a/packages/webapp/src/components/ui/Skeleton.tsx b/packages/webapp/src/components/ui/Skeleton.tsx new file mode 100644 index 0000000000..a7c3f9afa0 --- /dev/null +++ b/packages/webapp/src/components/ui/Skeleton.tsx @@ -0,0 +1,7 @@ +import { cn } from '../../utils/utils'; + +function Skeleton({ className, ...props }: React.HTMLAttributes) { + return
; +} + +export { Skeleton }; diff --git a/packages/webapp/src/components/ui/input/Input.tsx b/packages/webapp/src/components/ui/input/Input.tsx index 99112ba7c7..8bc34b8542 100644 --- a/packages/webapp/src/components/ui/input/Input.tsx +++ b/packages/webapp/src/components/ui/input/Input.tsx @@ -15,7 +15,7 @@ const Input = forwardRef< return (
@@ -24,7 +24,7 @@ const Input = forwardRef< type={type} ref={ref} className={cn( - 'bg-transparent h-full px-3 py-1.5 w-full text-white file:border-0 file:bg-transparent file:text-sm file:font-medium', + 'bg-transparent h-full px-3 py-2.5 w-full text-white file:border-0 file:bg-transparent file:text-sm file:font-medium', before && 'pl-8' )} {...props} diff --git a/packages/webapp/src/pages/Logs/Search.tsx b/packages/webapp/src/pages/Logs/Search.tsx index b098d14cc8..572bbb5cb3 100644 --- a/packages/webapp/src/pages/Logs/Search.tsx +++ b/packages/webapp/src/pages/Logs/Search.tsx @@ -13,6 +13,8 @@ import { useEffect, useState } from 'react'; import type { SearchOperationsState } from '@nangohq/types'; import Spinner from '../../components/ui/Spinner'; import { OperationRow } from './components/OperationRow'; +import { Input } from '../../components/ui/input/Input'; +import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; export const LogsSearch: React.FC = () => { const env = useStore((state) => state.env); @@ -74,7 +76,9 @@ export const LogsSearch: React.FC = () => {

Logs {loading && }

-
{/* } placeholder="Search logs..." /> */}
+
+ } placeholder="Search operations..." /> +
diff --git a/packages/webapp/src/pages/Logs/Show.tsx b/packages/webapp/src/pages/Logs/Show.tsx index 66577d250c..25fc76e2f3 100644 --- a/packages/webapp/src/pages/Logs/Show.tsx +++ b/packages/webapp/src/pages/Logs/Show.tsx @@ -1,6 +1,5 @@ import { useMemo } from 'react'; import Info from '../../components/ui/Info'; -import Spinner from '../../components/ui/Spinner'; import { useGetOperation } from '../../hooks/useLogs'; import { useStore } from '../../store'; import { OperationTag } from './components/OperationTag'; @@ -9,6 +8,7 @@ import { elapsedTime } from '../../utils/utils'; import { Link } from 'react-router-dom'; import { ExternalLinkIcon } from '@radix-ui/react-icons'; import { SearchInOperation } from './components/SearchInOperation'; +import { Skeleton } from '../../components/ui/Skeleton'; export const Show: React.FC<{ operationId: string }> = ({ operationId }) => { const env = useStore((state) => state.env); @@ -26,7 +26,21 @@ export const Show: React.FC<{ operationId: string }> = ({ operationId }) => { }, [operation]); if (loading) { - return ; + return ( +
+

Operation Details

+ + +
+

Payload

+ +
+
+

Logs

+ +
+
+ ); } if (error || !operation) { diff --git a/packages/webapp/src/pages/Logs/components/LevelTag.tsx b/packages/webapp/src/pages/Logs/components/LevelTag.tsx new file mode 100644 index 0000000000..b033ec08b6 --- /dev/null +++ b/packages/webapp/src/pages/Logs/components/LevelTag.tsx @@ -0,0 +1,31 @@ +import type { SearchMessagesData } from '@nangohq/types'; + +export const LevelTag: React.FC<{ level: SearchMessagesData['level'] }> = ({ level }) => { + if (level === 'error') { + return ( +
+
Error
+
+ ); + } else if (level === 'info') { + return ( +
+
Info
+
+ ); + } else if (level === 'warn') { + return ( +
+
Warn
+
+ ); + } else if (level === 'debug') { + return ( +
+
Debug
+
+ ); + } + + return null; +}; diff --git a/packages/webapp/src/pages/Logs/components/MessageRow.tsx b/packages/webapp/src/pages/Logs/components/MessageRow.tsx new file mode 100644 index 0000000000..8bc2185d31 --- /dev/null +++ b/packages/webapp/src/pages/Logs/components/MessageRow.tsx @@ -0,0 +1,33 @@ +import type { Row } from '@tanstack/react-table'; +import { flexRender } from '@tanstack/react-table'; +import type { SearchOperationsData } from '@nangohq/types'; + +import { Drawer, DrawerContent, DrawerTrigger, DrawerClose } from '../../../components/ui/Drawer'; +import * as Table from '../../../components/ui/Table'; +import { Show } from '../Show'; +import { Cross1Icon } from '@radix-ui/react-icons'; + +const drawerWidth = '1034px'; +export const MessageRow: React.FC<{ row: Row }> = ({ row }) => { + return ( + + + + {row.getVisibleCells().map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + ))} + + + +
+
+ + + +
+ +
+
+
+ ); +}; diff --git a/packages/webapp/src/pages/Logs/components/MessageTag.tsx b/packages/webapp/src/pages/Logs/components/MessageTag.tsx new file mode 100644 index 0000000000..e2df36067e --- /dev/null +++ b/packages/webapp/src/pages/Logs/components/MessageTag.tsx @@ -0,0 +1,19 @@ +import type { SearchMessagesData } from '@nangohq/types'; + +export const MessageTag: React.FC<{ type: SearchMessagesData['type'] }> = ({ type }) => { + if (type === 'log') { + return ( +
+
Message
+
+ ); + } else if (type === 'http') { + return ( +
+
HTTP
+
+ ); + } + + return null; +}; diff --git a/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx index 7748b6a045..c69762eaf5 100644 --- a/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx +++ b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx @@ -4,12 +4,14 @@ import { Input } from '../../../components/ui/input/Input'; import { useSearchMessages } from '../../../hooks/useLogs'; import type { SearchOperationsData } from '@nangohq/types'; import { formatDateToInternationalFormat } from '../../../utils/utils'; -import { StatusTag } from './StatusTag'; import { useStore } from '../../../store'; import * as Table from '../../../components/ui/Table'; import Spinner from '../../../components/ui/Spinner'; -import { OperationRow } from './OperationRow'; import Info from '../../../components/ui/Info'; +import { MessageTag } from './MessageTag'; +import { LevelTag } from './LevelTag'; +import { MessageRow } from './MessageRow'; +import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; export const columns: ColumnDef[] = [ { @@ -21,24 +23,25 @@ export const columns: ColumnDef[] = [ } }, { - accessorKey: 'state', - header: 'Status', - size: 100, + accessorKey: 'type', + header: 'Type', + size: 80, cell: ({ row }) => { - return ; + return ; } }, { - accessorKey: 'operation', - header: 'Type', - size: 100, + accessorKey: 'level', + header: 'Level', + size: 60, cell: ({ row }) => { - return row.original.type; + return ; } }, { accessorKey: 'message', header: 'Additional Info', + size: 'auto' as unknown as number, cell: ({ row }) => { return
{row.original.message}
; } @@ -53,14 +56,15 @@ export const SearchInOperation: React.FC<{ operationId: string }> = ({ operation const table = useReactTable({ data: data ? data.data : [], columns, + getCoreRowModel: getCoreRowModel() }); return (
-

Logs {loading && }

-
- +

Logs {loading && }

+
+ } placeholder="Search logs..." className="border-border-gray-400" />
{error && ( @@ -79,6 +83,7 @@ export const SearchInOperation: React.FC<{ operationId: string }> = ({ operation style={{ width: header.getSize() }} + className="bg-pure-black" > {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} @@ -89,7 +94,7 @@ export const SearchInOperation: React.FC<{ operationId: string }> = ({ operation {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ) + table.getRowModel().rows.map((row) => ) ) : ( diff --git a/packages/webapp/src/pages/Logs/components/StatusTag.tsx b/packages/webapp/src/pages/Logs/components/StatusTag.tsx index 1603d18259..b3eb35c5fd 100644 --- a/packages/webapp/src/pages/Logs/components/StatusTag.tsx +++ b/packages/webapp/src/pages/Logs/components/StatusTag.tsx @@ -4,7 +4,7 @@ export const StatusTag: React.FC<{ state: SearchOperationsData['state'] }> = ({ if (state === 'success') { return (
-
Success
+
Success
); } else if (state === 'running') { diff --git a/packages/webapp/tailwind.config.js b/packages/webapp/tailwind.config.js index 7dadbafe4a..6cd2aea016 100644 --- a/packages/webapp/tailwind.config.js +++ b/packages/webapp/tailwind.config.js @@ -32,7 +32,6 @@ module.exports = { 'dark-800': '#09090B', 'bg-dark-blue': '#182633', 'state-green-900': '#84D65A', - 'state-green-400': '#84D65A', 'row-hover': '#0d0d14', white: '#FFFFFF' }, From a804cea98c0098445a41556dac2a5c3fa7c66365 Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Thu, 16 May 2024 01:08:39 +0200 Subject: [PATCH 05/14] full text search --- package-lock.json | 269 ++++++++++++++++++ packages/logs/lib/models/messages.ts | 24 +- .../lib/controllers/v1/logs/searchMessages.ts | 8 +- packages/types/lib/logs/api.ts | 2 +- packages/webapp/package.json | 1 + packages/webapp/src/hooks/useLogs.tsx | 2 +- .../Logs/components/SearchInOperation.tsx | 17 +- 7 files changed, 308 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1808627b20..dd49595b01 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10410,6 +10410,12 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==", + "dev": true + }, "node_modules/@types/js-yaml": { "version": "4.0.5", "dev": true, @@ -11631,6 +11637,12 @@ "pluralize": "8.0.0" } }, + "node_modules/@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==", + "dev": true + }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "license": "BSD-3-Clause" @@ -14067,6 +14079,15 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dev": true, + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, "node_modules/copyfiles": { "version": "2.4.1", "license": "MIT", @@ -14326,6 +14347,15 @@ "postcss": "^8.4" } }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "dev": true, + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, "node_modules/css-loader": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", @@ -17213,6 +17243,18 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-loops": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.3.tgz", + "integrity": "sha512-8EZzEP0eKkEEVX+drtd9mtuQ+/QrlfW/5MlwcwK5Nds6EkZ/tRzEexkzUY2mIssnAyVLT+TKHuRXmFNNXYUd6g==", + "dev": true + }, + "node_modules/fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==", + "dev": true + }, "node_modules/fast-xml-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.4.tgz", @@ -17242,6 +17284,12 @@ "node": ">= 4.9.1" } }, + "node_modules/fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==", + "dev": true + }, "node_modules/fastq": { "version": "1.13.0", "dev": true, @@ -18956,6 +19004,12 @@ "node": ">=10.18" } }, + "node_modules/hyphenate-style-name": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.5.tgz", + "integrity": "sha512-fedL7PRwmeVkgyhu9hLeTBaI6wcGk7JGJswdaRsa5aUbkXI1kr1xZwTPBtaYPpwf56878iDek6VbVnuWMebJmw==", + "dev": true + }, "node_modules/iconv-lite": { "version": "0.4.24", "license": "MIT", @@ -19107,6 +19161,16 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "node_modules/inline-style-prefixer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.0.tgz", + "integrity": "sha512-I7GEdScunP1dQ6IM2mQWh6v0mOYdYmH3Bp31UecKdrcUgcURTcctSe1IECdUznSHKSmsHtjrT3CwCPI1pyxfUQ==", + "dev": true, + "dependencies": { + "css-in-js-utils": "^3.1.0", + "fast-loops": "^1.1.3" + } + }, "node_modules/int64-buffer": { "version": "0.1.10", "license": "MIT" @@ -21161,6 +21225,12 @@ "node": ">=10" } }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", + "dev": true + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -22703,6 +22773,66 @@ "resolved": "packages/cli", "link": true }, + "node_modules/nano-css": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.1.tgz", + "integrity": "sha512-T2Mhc//CepkTa3X4pUhKgbEheJHYAxD0VptuqFhDbGMUWVV2m+lkNiW/Ieuj35wrfC8Zm0l7HvssQh7zcEttSw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "css-tree": "^1.1.2", + "csstype": "^3.1.2", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^7.0.0", + "rtl-css-js": "^1.16.1", + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/nano-css/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/nano-css/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nano-css/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true + }, + "node_modules/nano-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nano-css/node_modules/stylis": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", + "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "dev": true + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -25828,6 +25958,48 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "dev": true, + "peerDependencies": { + "react": "*", + "tslib": "*" + } + }, + "node_modules/react-use": { + "version": "17.5.0", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.5.0.tgz", + "integrity": "sha512-PbfwSPMwp/hoL847rLnm/qkjg3sTRCvn6YhUZiHaUa3FA6/aNoFX79ul5Xt70O1rK+9GxSVqkY0eTwMdsR/bWg==", + "dev": true, + "dependencies": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.6.1", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/react-use/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, "node_modules/read": { "version": "1.0.7", "license": "ISC", @@ -26100,6 +26272,12 @@ "version": "1.0.0", "license": "MIT" }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -26299,6 +26477,15 @@ "fsevents": "~2.3.2" } }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/run-async": { "version": "3.0.0", "license": "MIT", @@ -26532,6 +26719,18 @@ "dev": true, "license": "MIT" }, + "node_modules/screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/secure-json-parse": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", @@ -26768,6 +26967,15 @@ "node": ">= 0.4" } }, + "node_modules/set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==", + "dev": true, + "engines": { + "node": ">=6.9" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "license": "ISC" @@ -27118,6 +27326,15 @@ "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", "dev": true }, + "node_modules/stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "dev": true, + "dependencies": { + "stackframe": "^1.3.4" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "license": "MIT", @@ -27157,6 +27374,36 @@ "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", "dev": true }, + "node_modules/stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "dev": true, + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "node_modules/stacktrace-gps/node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "dev": true, + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, "node_modules/static-eval": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", @@ -28323,6 +28570,15 @@ "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==", "dev": true }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/through2": { "version": "2.0.5", "license": "MIT", @@ -28441,6 +28697,12 @@ "node": ">=8.0" } }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "dev": true + }, "node_modules/toidentifier": { "version": "1.0.1", "license": "MIT", @@ -28517,6 +28779,12 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==", + "dev": true + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "dev": true, @@ -33340,6 +33608,7 @@ "react-router-dom": "6.8.2", "react-scripts": "5.0.1", "react-toastify": "9.1.1", + "react-use": "17.5.0", "swr": "2.2.5", "tailwind-merge": "2.2.1", "tailwindcss": "3.2.7", diff --git a/packages/logs/lib/models/messages.ts b/packages/logs/lib/models/messages.ts index dfe1296e88..b54940bcc2 100644 --- a/packages/logs/lib/models/messages.ts +++ b/packages/logs/lib/models/messages.ts @@ -47,6 +47,7 @@ export async function listOperations(opts: { (query.bool!.must as opensearchtypes.QueryDslQueryContainer[]).push({ term: { environmentId: opts.environmentId } }); } if (opts.states && (opts.states.length > 1 || opts.states[0] !== 'all')) { + // Where or (query.bool!.must as opensearchtypes.QueryDslQueryContainer[]).push({ bool: { should: opts.states.map((state) => { @@ -139,7 +140,12 @@ export async function setTimeouted(opts: Pick): Promise /** * List messages */ -export async function listMessages(opts: { parentId: string; limit: number; states?: SearchOperationsState[] | undefined }): Promise { +export async function listMessages(opts: { + parentId: string; + limit: number; + states?: SearchOperationsState[] | undefined; + search?: string | undefined; +}): Promise { const query: opensearchtypes.QueryDslQueryContainer = { bool: { must: [{ term: { parentId: opts.parentId } }], @@ -148,6 +154,7 @@ export async function listMessages(opts: { parentId: string; limit: number; stat }; if (opts.states && (opts.states.length > 1 || opts.states[0] !== 'all')) { + // Where or (query.bool!.must as opensearchtypes.QueryDslQueryContainer[]).push({ bool: { should: opts.states.map((state) => { @@ -156,19 +163,20 @@ export async function listMessages(opts: { parentId: string; limit: number; stat } }); } + if (opts.search) { + (query.bool!.must as opensearchtypes.QueryDslQueryContainer[]).push({ + match_phrase_prefix: { message: { query: opts.search } } + }); + } + + console.log(JSON.stringify(query)); const res = await client.search<{ hits: { total: number; hits: { _source: MessageRow }[] } }>({ index: indexMessages.index, size: opts.limit, sort: ['createdAt:desc', '_score'], track_total_hits: true, - body: { - query: { - bool: { - must: [{ term: { parentId: opts.parentId } }] - } - } - } + body: { query } }); const hits = res.body.hits; diff --git a/packages/server/lib/controllers/v1/logs/searchMessages.ts b/packages/server/lib/controllers/v1/logs/searchMessages.ts index 757cf39c6a..093ec7a570 100644 --- a/packages/server/lib/controllers/v1/logs/searchMessages.ts +++ b/packages/server/lib/controllers/v1/logs/searchMessages.ts @@ -8,6 +8,7 @@ const validation = z .object({ operationId: z.string().regex(operationIdRegex), limit: z.number().optional().default(100), + search: z.string().optional(), states: z .array(z.enum(['all', 'waiting', 'running', 'success', 'failed', 'timeout', 'cancelled'])) .optional() @@ -54,7 +55,12 @@ export const searchMessages = asyncWrapper(async (req, res) => { } const body: SearchMessages['Body'] = val.data; - const rawOps = await model.listMessages({ parentId: body.operationId, limit: body.limit!, states: body.states }); + const rawOps = await model.listMessages({ + parentId: body.operationId, + limit: body.limit!, + states: body.states, + search: body.search + }); res.status(200).send({ data: rawOps.items, diff --git a/packages/types/lib/logs/api.ts b/packages/types/lib/logs/api.ts index 4a6b9fa0fe..81c13fc062 100644 --- a/packages/types/lib/logs/api.ts +++ b/packages/types/lib/logs/api.ts @@ -28,7 +28,7 @@ export type SearchMessages = Endpoint<{ Method: 'POST'; Path: '/api/v1/logs/messages'; Querystring: { env: string }; - Body: { operationId: string; limit?: number; states?: SearchOperationsState[] }; + Body: { operationId: string; limit?: number; states?: SearchOperationsState[]; search?: string | undefined }; Success: { data: MessageRow[]; pagination: { total: number }; diff --git a/packages/webapp/package.json b/packages/webapp/package.json index 1e0def761f..55d5acf361 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -66,6 +66,7 @@ "react-router-dom": "6.8.2", "react-scripts": "5.0.1", "react-toastify": "9.1.1", + "react-use": "17.5.0", "swr": "2.2.5", "tailwind-merge": "2.2.1", "tailwindcss": "3.2.7", diff --git a/packages/webapp/src/hooks/useLogs.tsx b/packages/webapp/src/hooks/useLogs.tsx index cdc075ea9c..e37d66cfe8 100644 --- a/packages/webapp/src/hooks/useLogs.tsx +++ b/packages/webapp/src/hooks/useLogs.tsx @@ -88,7 +88,7 @@ export function useSearchMessages(env: string, body: SearchMessages['Body']) { if (!loading) { void fetchData(); } - }, [env, body.operationId, body.limit, body.states]); + }, [env, body.operationId, body.limit, body.states, body.search]); return { data, error, loading }; } diff --git a/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx index c69762eaf5..56544072dd 100644 --- a/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx +++ b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx @@ -12,6 +12,8 @@ import { MessageTag } from './MessageTag'; import { LevelTag } from './LevelTag'; import { MessageRow } from './MessageRow'; import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; +import { useState } from 'react'; +import { useDebounce } from 'react-use'; export const columns: ColumnDef[] = [ { @@ -33,7 +35,7 @@ export const columns: ColumnDef[] = [ { accessorKey: 'level', header: 'Level', - size: 60, + size: 70, cell: ({ row }) => { return ; } @@ -51,20 +53,27 @@ export const columns: ColumnDef[] = [ export const SearchInOperation: React.FC<{ operationId: string }> = ({ operationId }) => { const env = useStore((state) => state.env); - const { data, error, loading } = useSearchMessages(env, { limit: 20, operationId }); + const [search, setSearch] = useState(); + const [debouncedSearch, setDebouncedSearch] = useState(); + const { data, error, loading } = useSearchMessages(env, { limit: 20, operationId, search: debouncedSearch }); const table = useReactTable({ data: data ? data.data : [], columns, - getCoreRowModel: getCoreRowModel() }); + useDebounce(() => setDebouncedSearch(search), 250, [search]); return (

Logs {loading && }

- } placeholder="Search logs..." className="border-border-gray-400" /> + } + placeholder="Search logs..." + className="border-border-gray-400" + onChange={(e) => setSearch(e.target.value)} + />
{error && ( From 3f04d9e6c1e7743c5729b6f221d931a686feadc6 Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Thu, 16 May 2024 12:37:49 +0200 Subject: [PATCH 06/14] review --- packages/webapp/src/components/ui/Table.tsx | 2 +- .../src/components/ui/button/Button.tsx | 8 +-- .../webapp/src/components/ui/input/Input.tsx | 4 +- packages/webapp/src/pages/Logs/Search.tsx | 18 +++++- packages/webapp/src/pages/Logs/Show.tsx | 60 ++++++++++++------- .../src/pages/Logs/components/MultiSelect.tsx | 4 +- .../pages/Logs/components/OperationTag.tsx | 2 +- .../Logs/components/SearchInOperation.tsx | 20 +++++-- .../src/pages/Logs/components/StatusTag.tsx | 12 ++-- packages/webapp/src/pages/Logs/constants.tsx | 22 +++---- packages/webapp/src/utils/utils.tsx | 5 +- packages/webapp/tailwind.config.js | 4 ++ 12 files changed, 102 insertions(+), 59 deletions(-) diff --git a/packages/webapp/src/components/ui/Table.tsx b/packages/webapp/src/components/ui/Table.tsx index 74b74d1e0e..9ba34d2183 100644 --- a/packages/webapp/src/components/ui/Table.tsx +++ b/packages/webapp/src/components/ui/Table.tsx @@ -27,7 +27,7 @@ const Row = forwardRef(function Button({ size } return ( - + {options.map((option) => { diff --git a/packages/webapp/src/pages/Logs/components/OperationTag.tsx b/packages/webapp/src/pages/Logs/components/OperationTag.tsx index 38ac6c16e2..fb24f60c1c 100644 --- a/packages/webapp/src/pages/Logs/components/OperationTag.tsx +++ b/packages/webapp/src/pages/Logs/components/OperationTag.tsx @@ -6,7 +6,7 @@ export const OperationTag: React.FC<{ operation: Exclude diff --git a/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx index 56544072dd..fcb910e89c 100644 --- a/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx +++ b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx @@ -3,7 +3,7 @@ import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-tabl import { Input } from '../../../components/ui/input/Input'; import { useSearchMessages } from '../../../hooks/useLogs'; import type { SearchOperationsData } from '@nangohq/types'; -import { formatDateToInternationalFormat } from '../../../utils/utils'; +import { formatDateToLogFormat } from '../../../utils/utils'; import { useStore } from '../../../store'; import * as Table from '../../../components/ui/Table'; import Spinner from '../../../components/ui/Spinner'; @@ -11,7 +11,7 @@ import Info from '../../../components/ui/Info'; import { MessageTag } from './MessageTag'; import { LevelTag } from './LevelTag'; import { MessageRow } from './MessageRow'; -import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; +import { ChevronRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons'; import { useState } from 'react'; import { useDebounce } from 'react-use'; @@ -19,9 +19,9 @@ export const columns: ColumnDef[] = [ { accessorKey: 'createdAt', header: 'Timestamp', - size: 120, + size: 170, cell: ({ row }) => { - return formatDateToInternationalFormat(row.original.createdAt); + return
{formatDateToLogFormat(row.original.createdAt)}
; } }, { @@ -47,6 +47,18 @@ export const columns: ColumnDef[] = [ cell: ({ row }) => { return
{row.original.message}
; } + }, + { + accessorKey: 'id', + header: '', + size: 40, + cell: () => { + return ( +
+ +
+ ); + } } ]; diff --git a/packages/webapp/src/pages/Logs/components/StatusTag.tsx b/packages/webapp/src/pages/Logs/components/StatusTag.tsx index b3eb35c5fd..c331bc86ea 100644 --- a/packages/webapp/src/pages/Logs/components/StatusTag.tsx +++ b/packages/webapp/src/pages/Logs/components/StatusTag.tsx @@ -4,37 +4,37 @@ export const StatusTag: React.FC<{ state: SearchOperationsData['state'] }> = ({ if (state === 'success') { return (
-
Success
+
Success
); } else if (state === 'running') { return (
-
Running
+
Running
); } else if (state === 'cancelled') { return (
-
Cancelled
+
Cancelled
); } else if (state === 'failed') { return (
-
Failed
+
Failed
); } else if (state === 'timeout') { return (
-
Timeout
+
Timeout
); } else if (state === 'waiting') { return (
-
Waiting
+
Waiting
); } diff --git a/packages/webapp/src/pages/Logs/constants.tsx b/packages/webapp/src/pages/Logs/constants.tsx index 9989bed742..12c802d856 100644 --- a/packages/webapp/src/pages/Logs/constants.tsx +++ b/packages/webapp/src/pages/Logs/constants.tsx @@ -1,24 +1,24 @@ import type { ColumnDef } from '@tanstack/react-table'; import type { SearchOperationsData, SearchOperationsState } from '@nangohq/types'; -import { formatDateToInternationalFormat } from '../../utils/utils'; +import { formatDateToLogFormat } from '../../utils/utils'; import { StatusTag } from './components/StatusTag'; import { OperationTag } from './components/OperationTag'; import type { MultiSelectArgs } from './components/MultiSelect'; -import { ChevronRight } from '@geist-ui/icons'; +import { ChevronRightIcon } from '@radix-ui/react-icons'; export const columns: ColumnDef[] = [ { accessorKey: 'createdAt', header: 'Timestamp', - size: 120, + size: 170, cell: ({ row }) => { - return formatDateToInternationalFormat(row.original.createdAt); + return
{formatDateToLogFormat(row.original.createdAt)}
; } }, { accessorKey: 'state', header: 'Status', - size: 100, + size: 90, cell: ({ row }) => { return ; } @@ -36,15 +36,15 @@ export const columns: ColumnDef[] = [ header: 'Integration', size: 200, cell: ({ row }) => { - return
{row.original.configName}
; + return
{row.original.configName}
; } }, { accessorKey: 'syncId', header: 'Script', - size: 200, + size: 180, cell: ({ row }) => { - return
{row.original.syncName}
; + return
{row.original.syncName}
; } }, { @@ -52,17 +52,17 @@ export const columns: ColumnDef[] = [ header: 'Connection', size: 200, cell: ({ row }) => { - return
{row.original.connectionName}
; + return
{row.original.connectionName}
; } }, { accessorKey: 'id', header: '', - size: 20, + size: 40, cell: () => { return (
- +
); } diff --git a/packages/webapp/src/utils/utils.tsx b/packages/webapp/src/utils/utils.tsx index 938ad63097..bdbaa0822c 100644 --- a/packages/webapp/src/utils/utils.tsx +++ b/packages/webapp/src/utils/utils.tsx @@ -146,7 +146,7 @@ export function formatDateToUSFormat(dateString: string): string { return formattedDate; } -export function formatDateToInternationalFormat(dateString: string): string { +export function formatDateToLogFormat(dateString: string): string { const date = new Date(dateString); const options: Intl.DateTimeFormatOptions = { hour: '2-digit', @@ -154,6 +154,7 @@ export function formatDateToInternationalFormat(dateString: string): string { second: '2-digit', month: 'short', day: '2-digit', + fractionalSecondDigits: 2, hour12: false }; @@ -164,7 +165,7 @@ export function formatDateToInternationalFormat(dateString: string): string { } const parts = formattedDate.split(', '); - return `${parts[0]}, ${parts[1]}`; + return `${parts[0]} ${parts[1]}`; } export function parseCron(frequency: string): string { diff --git a/packages/webapp/tailwind.config.js b/packages/webapp/tailwind.config.js index 6cd2aea016..628d335a3a 100644 --- a/packages/webapp/tailwind.config.js +++ b/packages/webapp/tailwind.config.js @@ -40,7 +40,11 @@ module.exports = { largecell: '480px' }, fontSize: { + s: '13px', '3xl': '28px' + }, + fontFamily: { + code: ['"Roboto Mono"', '"Source Code Pro"', 'system-ui', 'sans-serif'] } } }, From 7e34e5b76c0d652b0ff9cbba2aed279ab8694984 Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Thu, 16 May 2024 14:53:00 +0200 Subject: [PATCH 07/14] more --- .../webapp/src/pages/Logs/ShowMessage.tsx | 59 +++++++++++++++++++ .../Logs/{Show.tsx => ShowOperation.tsx} | 6 +- .../src/pages/Logs/components/MessageRow.tsx | 16 ++--- .../pages/Logs/components/OperationRow.tsx | 8 +-- .../src/pages/Logs/components/SourceTag.tsx | 19 ++++++ 5 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 packages/webapp/src/pages/Logs/ShowMessage.tsx rename packages/webapp/src/pages/Logs/{Show.tsx => ShowOperation.tsx} (96%) create mode 100644 packages/webapp/src/pages/Logs/components/SourceTag.tsx diff --git a/packages/webapp/src/pages/Logs/ShowMessage.tsx b/packages/webapp/src/pages/Logs/ShowMessage.tsx new file mode 100644 index 0000000000..dce5319889 --- /dev/null +++ b/packages/webapp/src/pages/Logs/ShowMessage.tsx @@ -0,0 +1,59 @@ +import type { MessageRow } from '@nangohq/types'; +import { useMemo } from 'react'; +import { formatDateToLogFormat } from '../../utils/utils'; +import { SourceTag } from './components/SourceTag'; +import { Prism } from '@mantine/prism'; +import { LevelTag } from './components/LevelTag'; + +export const ShowMessage: React.FC<{ message: MessageRow }> = ({ message }) => { + const createdAt = useMemo(() => { + return formatDateToLogFormat(message.createdAt); + }, [message.createdAt]); + + return ( +
+
+

{message.type === 'log' ? 'Message' : 'HTTP'} Details

+
+ +
+
+
Timestamp
+
{createdAt}
+
+
+
Status
+
+ +
+
+
+
Source
+
+ +
+
+
+
+

Message

+
{message.message}
+
+
+

Error

+ +
+ { + return { code: { padding: '0', whiteSpace: 'pre-wrap' } }; + }} + > + {JSON.stringify(message.error, null, 2)} + +
+
+
+ ); +}; diff --git a/packages/webapp/src/pages/Logs/Show.tsx b/packages/webapp/src/pages/Logs/ShowOperation.tsx similarity index 96% rename from packages/webapp/src/pages/Logs/Show.tsx rename to packages/webapp/src/pages/Logs/ShowOperation.tsx index 955f00beb1..d1c70c2502 100644 --- a/packages/webapp/src/pages/Logs/Show.tsx +++ b/packages/webapp/src/pages/Logs/ShowOperation.tsx @@ -10,7 +10,7 @@ import { ExternalLinkIcon } from '@radix-ui/react-icons'; import { SearchInOperation } from './components/SearchInOperation'; import { Skeleton } from '../../components/ui/Skeleton'; -export const Show: React.FC<{ operationId: string }> = ({ operationId }) => { +export const ShowOperation: React.FC<{ operationId: string }> = ({ operationId }) => { const env = useStore((state) => state.env); const { operation, loading, error } = useGetOperation(env, { operationId }); @@ -57,7 +57,7 @@ export const Show: React.FC<{ operationId: string }> = ({ operationId }) => { } return ( -
+

Operation Details

@@ -120,7 +120,7 @@ export const Show: React.FC<{ operationId: string }> = ({ operationId }) => {

Payload

- {!operation.meta &&
No payload.
} + {!operation.meta &&
No payload.
}
diff --git a/packages/webapp/src/pages/Logs/components/MessageRow.tsx b/packages/webapp/src/pages/Logs/components/MessageRow.tsx index 8bc2185d31..704e527f78 100644 --- a/packages/webapp/src/pages/Logs/components/MessageRow.tsx +++ b/packages/webapp/src/pages/Logs/components/MessageRow.tsx @@ -4,10 +4,10 @@ import type { SearchOperationsData } from '@nangohq/types'; import { Drawer, DrawerContent, DrawerTrigger, DrawerClose } from '../../../components/ui/Drawer'; import * as Table from '../../../components/ui/Table'; -import { Show } from '../Show'; -import { Cross1Icon } from '@radix-ui/react-icons'; +import { ShowMessage } from '../ShowMessage'; +import { ArrowLeftIcon } from '@radix-ui/react-icons'; -const drawerWidth = '1034px'; +const drawerWidth = '834px'; export const MessageRow: React.FC<{ row: Row }> = ({ row }) => { return ( @@ -19,13 +19,13 @@ export const MessageRow: React.FC<{ row: Row }> = ({ row } -
-
- - +
+
+ +
- +
diff --git a/packages/webapp/src/pages/Logs/components/OperationRow.tsx b/packages/webapp/src/pages/Logs/components/OperationRow.tsx index 5c9b66717f..36f3a03143 100644 --- a/packages/webapp/src/pages/Logs/components/OperationRow.tsx +++ b/packages/webapp/src/pages/Logs/components/OperationRow.tsx @@ -4,7 +4,7 @@ import type { SearchOperationsData } from '@nangohq/types'; import { Drawer, DrawerContent, DrawerTrigger, DrawerClose } from '../../../components/ui/Drawer'; import * as Table from '../../../components/ui/Table'; -import { Show } from '../Show'; +import { ShowOperation } from '../ShowOperation'; import { Cross1Icon } from '@radix-ui/react-icons'; const drawerWidth = '1034px'; @@ -21,11 +21,11 @@ export const OperationRow: React.FC<{ row: Row }> = ({ row
- - + +
- +
diff --git a/packages/webapp/src/pages/Logs/components/SourceTag.tsx b/packages/webapp/src/pages/Logs/components/SourceTag.tsx new file mode 100644 index 0000000000..fffb313dc1 --- /dev/null +++ b/packages/webapp/src/pages/Logs/components/SourceTag.tsx @@ -0,0 +1,19 @@ +import type { SearchMessagesData } from '@nangohq/types'; + +export const SourceTag: React.FC<{ source: SearchMessagesData['source'] }> = ({ source }) => { + if (source === 'internal') { + return ( +
+
System
+
+ ); + } else if (source === 'user') { + return ( +
+
User
+
+ ); + } + + return null; +}; From 665ffa2f023560af63cd15512851eb9a0619f090 Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Thu, 16 May 2024 18:48:22 +0200 Subject: [PATCH 08/14] Tag --- .../webapp/src/pages/Logs/ShowMessage.tsx | 4 +- .../src/pages/Logs/components/LevelTag.tsx | 25 +++++++------ .../src/pages/Logs/components/MessageTag.tsx | 19 ---------- .../pages/Logs/components/OperationTag.tsx | 37 +++++++++++-------- .../Logs/components/SearchInOperation.tsx | 4 +- .../src/pages/Logs/components/SourceTag.tsx | 19 ---------- .../src/pages/Logs/components/StatusTag.tsx | 37 ++++++++++--------- .../webapp/src/pages/Logs/components/Tag.tsx | 14 +++++++ packages/webapp/tailwind.config.js | 6 ++- 9 files changed, 76 insertions(+), 89 deletions(-) delete mode 100644 packages/webapp/src/pages/Logs/components/MessageTag.tsx delete mode 100644 packages/webapp/src/pages/Logs/components/SourceTag.tsx create mode 100644 packages/webapp/src/pages/Logs/components/Tag.tsx diff --git a/packages/webapp/src/pages/Logs/ShowMessage.tsx b/packages/webapp/src/pages/Logs/ShowMessage.tsx index dce5319889..240841e63e 100644 --- a/packages/webapp/src/pages/Logs/ShowMessage.tsx +++ b/packages/webapp/src/pages/Logs/ShowMessage.tsx @@ -1,9 +1,9 @@ import type { MessageRow } from '@nangohq/types'; import { useMemo } from 'react'; import { formatDateToLogFormat } from '../../utils/utils'; -import { SourceTag } from './components/SourceTag'; import { Prism } from '@mantine/prism'; import { LevelTag } from './components/LevelTag'; +import { Tag } from './components/Tag'; export const ShowMessage: React.FC<{ message: MessageRow }> = ({ message }) => { const createdAt = useMemo(() => { @@ -30,7 +30,7 @@ export const ShowMessage: React.FC<{ message: MessageRow }> = ({ message }) => {
Source
- + {message.source === 'internal' ? 'System' : 'User'}
diff --git a/packages/webapp/src/pages/Logs/components/LevelTag.tsx b/packages/webapp/src/pages/Logs/components/LevelTag.tsx index b033ec08b6..c70d5d247d 100644 --- a/packages/webapp/src/pages/Logs/components/LevelTag.tsx +++ b/packages/webapp/src/pages/Logs/components/LevelTag.tsx @@ -1,29 +1,30 @@ import type { SearchMessagesData } from '@nangohq/types'; +import { Tag } from './Tag'; export const LevelTag: React.FC<{ level: SearchMessagesData['level'] }> = ({ level }) => { if (level === 'error') { return ( -
-
Error
-
+ + Error + ); } else if (level === 'info') { return ( -
-
Info
-
+ + Info + ); } else if (level === 'warn') { return ( -
-
Warn
-
+ + Warn + ); } else if (level === 'debug') { return ( -
-
Debug
-
+ + Debug + ); } diff --git a/packages/webapp/src/pages/Logs/components/MessageTag.tsx b/packages/webapp/src/pages/Logs/components/MessageTag.tsx deleted file mode 100644 index e2df36067e..0000000000 --- a/packages/webapp/src/pages/Logs/components/MessageTag.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { SearchMessagesData } from '@nangohq/types'; - -export const MessageTag: React.FC<{ type: SearchMessagesData['type'] }> = ({ type }) => { - if (type === 'log') { - return ( -
-
Message
-
- ); - } else if (type === 'http') { - return ( -
-
HTTP
-
- ); - } - - return null; -}; diff --git a/packages/webapp/src/pages/Logs/components/OperationTag.tsx b/packages/webapp/src/pages/Logs/components/OperationTag.tsx index fb24f60c1c..3451792628 100644 --- a/packages/webapp/src/pages/Logs/components/OperationTag.tsx +++ b/packages/webapp/src/pages/Logs/components/OperationTag.tsx @@ -1,24 +1,31 @@ // import { ChevronRight } from '@geist-ui/icons'; import type { SearchOperationsData } from '@nangohq/types'; import { cn } from '../../../utils/utils'; +import { Tag } from './Tag'; +import { CrossCircledIcon, LoopIcon, PauseIcon, PlayIcon, ResumeIcon, UploadIcon } from '@radix-ui/react-icons'; export const OperationTag: React.FC<{ operation: Exclude; highlight?: boolean }> = ({ operation, highlight }) => { + if (operation.type === 'sync') { + return ( +
+ + {operation.type} + + + {operation.action === 'cancel' && } + {operation.action === 'init' && } + {operation.action === 'pause' && } + {operation.action === 'run' && } + {operation.action === 'run_full' && } + {operation.action === 'unpause' && } + +
+ ); + } + return ( -
+ {operation.type} - {/* {'action' in operation && ( - <> - - {operation.action} - - )} */} -
+ ); - - return null; }; diff --git a/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx index fcb910e89c..52bb1648c3 100644 --- a/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx +++ b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx @@ -8,12 +8,12 @@ import { useStore } from '../../../store'; import * as Table from '../../../components/ui/Table'; import Spinner from '../../../components/ui/Spinner'; import Info from '../../../components/ui/Info'; -import { MessageTag } from './MessageTag'; import { LevelTag } from './LevelTag'; import { MessageRow } from './MessageRow'; import { ChevronRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons'; import { useState } from 'react'; import { useDebounce } from 'react-use'; +import { Tag } from './Tag'; export const columns: ColumnDef[] = [ { @@ -29,7 +29,7 @@ export const columns: ColumnDef[] = [ header: 'Type', size: 80, cell: ({ row }) => { - return ; + return {row.original.type === 'log' ? 'Message' : 'HTTP'}; } }, { diff --git a/packages/webapp/src/pages/Logs/components/SourceTag.tsx b/packages/webapp/src/pages/Logs/components/SourceTag.tsx deleted file mode 100644 index fffb313dc1..0000000000 --- a/packages/webapp/src/pages/Logs/components/SourceTag.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type { SearchMessagesData } from '@nangohq/types'; - -export const SourceTag: React.FC<{ source: SearchMessagesData['source'] }> = ({ source }) => { - if (source === 'internal') { - return ( -
-
System
-
- ); - } else if (source === 'user') { - return ( -
-
User
-
- ); - } - - return null; -}; diff --git a/packages/webapp/src/pages/Logs/components/StatusTag.tsx b/packages/webapp/src/pages/Logs/components/StatusTag.tsx index c331bc86ea..9d55a30b05 100644 --- a/packages/webapp/src/pages/Logs/components/StatusTag.tsx +++ b/packages/webapp/src/pages/Logs/components/StatusTag.tsx @@ -1,41 +1,42 @@ import type { SearchOperationsData } from '@nangohq/types'; +import { Tag } from './Tag'; export const StatusTag: React.FC<{ state: SearchOperationsData['state'] }> = ({ state }) => { if (state === 'success') { return ( -
-
Success
-
+ + Success + ); } else if (state === 'running') { return ( -
-
Running
-
+ + Running + ); } else if (state === 'cancelled') { return ( -
-
Cancelled
-
+ + Cancelled + ); } else if (state === 'failed') { return ( -
-
Failed
-
+ + Failed + ); } else if (state === 'timeout') { return ( -
-
Timeout
-
+ + Timeout + ); } else if (state === 'waiting') { return ( -
-
Waiting
-
+ + Waiting + ); } diff --git a/packages/webapp/src/pages/Logs/components/Tag.tsx b/packages/webapp/src/pages/Logs/components/Tag.tsx new file mode 100644 index 0000000000..3e1c1f1f8f --- /dev/null +++ b/packages/webapp/src/pages/Logs/components/Tag.tsx @@ -0,0 +1,14 @@ +import type { HTMLAttributes } from 'react'; +import { cn } from '../../../utils/utils'; + +export const Tag: React.FC<{ + children: React.ReactNode; + bgClassName?: HTMLAttributes['className']; + textClassName?: HTMLAttributes['className']; +}> = ({ children, bgClassName, textClassName }) => { + return ( +
+
{children}
+
+ ); +}; diff --git a/packages/webapp/tailwind.config.js b/packages/webapp/tailwind.config.js index 628d335a3a..1acd8291ce 100644 --- a/packages/webapp/tailwind.config.js +++ b/packages/webapp/tailwind.config.js @@ -31,9 +31,11 @@ module.exports = { 'dark-700': '#18181B', 'dark-800': '#09090B', 'bg-dark-blue': '#182633', - 'state-green-900': '#84D65A', 'row-hover': '#0d0d14', - white: '#FFFFFF' + white: '#FFFFFF', + + // From Figma + 'green-base': '#84D65A' }, width: { largebox: '1200px', From 52108a0d18e021bb0ea75d3526fd09baa498bf6a Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Tue, 21 May 2024 11:12:53 +0200 Subject: [PATCH 09/14] wip --- package-lock.json | 252 ++++++++++++++++++ packages/webapp/package.json | 1 + packages/webapp/src/App.tsx | 105 ++++---- packages/webapp/src/components/ui/Tooltip.tsx | 26 ++ packages/webapp/src/pages/Logs/Search.tsx | 5 +- .../webapp/src/pages/Logs/ShowMessage.tsx | 33 ++- .../webapp/src/pages/Logs/ShowOperation.tsx | 67 ++--- .../pages/Logs/components/OperationRow.tsx | 2 +- .../pages/Logs/components/OperationTag.tsx | 28 +- .../Logs/components/SearchInOperation.tsx | 16 +- packages/webapp/src/utils/utils.tsx | 5 + 11 files changed, 425 insertions(+), 115 deletions(-) create mode 100644 packages/webapp/src/components/ui/Tooltip.tsx diff --git a/package-lock.json b/package-lock.json index 56119f7738..1ce8c0ed02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8636,6 +8636,172 @@ "react": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", + "integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-popper": "1.1.3", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-slot": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", + "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", @@ -8792,6 +8958,91 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", + "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/rect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", @@ -33659,6 +33910,7 @@ "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-icons": "1.3.0", "@radix-ui/react-popover": "1.0.7", + "@radix-ui/react-tooltip": "1.0.7", "@sentry/react": "7.83.0", "@tailwindcss/forms": "0.5.3", "@tanstack/react-table": "8.16.0", diff --git a/packages/webapp/package.json b/packages/webapp/package.json index 09c0dd4df6..f90f11975d 100644 --- a/packages/webapp/package.json +++ b/packages/webapp/package.json @@ -40,6 +40,7 @@ "@radix-ui/react-dropdown-menu": "2.0.6", "@radix-ui/react-icons": "1.3.0", "@radix-ui/react-popover": "1.0.7", + "@radix-ui/react-tooltip": "1.0.7", "@sentry/react": "7.83.0", "@tailwindcss/forms": "0.5.3", "@tanstack/react-table": "8.16.0", diff --git a/packages/webapp/src/App.tsx b/packages/webapp/src/App.tsx index e0531fd8c9..8b3e4903a1 100644 --- a/packages/webapp/src/App.tsx +++ b/packages/webapp/src/App.tsx @@ -31,6 +31,7 @@ import UserSettings from './pages/UserSettings'; import { Homepage } from './pages/Homepage'; import { NotFound } from './pages/NotFound'; import { LogsSearch } from './pages/Logs/Search'; +import { TooltipProvider } from '@radix-ui/react-tooltip'; Sentry.init({ dsn: process.env.REACT_APP_PUBLIC_SENTRY_KEY, @@ -59,62 +60,64 @@ const App = () => { return ( - { - if (error.status === 401) { - return signout(); + + { + if (error.status === 401) { + return signout(); + } } - } - }} - > - - } /> - }> - {showInteractiveDemo && ( - }> - } /> - - )} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + }} + > + + } /> + }> + {showInteractiveDemo && ( + }> + } /> + + )} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {AUTH_ENABLED && ( + <> + } /> + } /> + + )} + + } /> + {true && } />} {AUTH_ENABLED && ( <> - } /> - } /> + } /> + } /> + } /> + } /> )} - - } /> - {true && } />} - {AUTH_ENABLED && ( - <> - } /> - } /> - } /> - } /> - - )} - {(isCloud() || isLocal()) && } />} - } /> - - - + {(isCloud() || isLocal()) && } />} + } /> + + + + ); }; diff --git a/packages/webapp/src/components/ui/Tooltip.tsx b/packages/webapp/src/components/ui/Tooltip.tsx new file mode 100644 index 0000000000..3c24d1a9cf --- /dev/null +++ b/packages/webapp/src/components/ui/Tooltip.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import * as TooltipPrimitive from '@radix-ui/react-tooltip'; +import { cn } from '../../utils/utils'; + +const TooltipProvider = TooltipPrimitive.Provider; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +const TooltipContent = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, sideOffset = 4, ...props }, ref) => ( + + ) +); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/packages/webapp/src/pages/Logs/Search.tsx b/packages/webapp/src/pages/Logs/Search.tsx index cd73a8201f..d131ec51fb 100644 --- a/packages/webapp/src/pages/Logs/Search.tsx +++ b/packages/webapp/src/pages/Logs/Search.tsx @@ -15,8 +15,7 @@ import Spinner from '../../components/ui/Spinner'; import { OperationRow } from './components/OperationRow'; import { Input } from '../../components/ui/input/Input'; import { MagnifyingGlassIcon } from '@radix-ui/react-icons'; - -const formatter = Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1, minimumFractionDigits: 0 }); +import { formatQuantity } from '../../utils/utils'; export const LogsSearch: React.FC = () => { const env = useStore((state) => state.env); @@ -44,7 +43,7 @@ export const LogsSearch: React.FC = () => { if (!data?.pagination) { return 0; } - return formatter.format(data.pagination.total); + return formatQuantity(data.pagination.total); }, [data?.pagination]); if (error) { diff --git a/packages/webapp/src/pages/Logs/ShowMessage.tsx b/packages/webapp/src/pages/Logs/ShowMessage.tsx index 240841e63e..83b36c8efc 100644 --- a/packages/webapp/src/pages/Logs/ShowMessage.tsx +++ b/packages/webapp/src/pages/Logs/ShowMessage.tsx @@ -4,6 +4,7 @@ import { formatDateToLogFormat } from '../../utils/utils'; import { Prism } from '@mantine/prism'; import { LevelTag } from './components/LevelTag'; import { Tag } from './components/Tag'; +import { CalendarIcon } from '@radix-ui/react-icons'; export const ShowMessage: React.FC<{ message: MessageRow }> = ({ message }) => { const createdAt = useMemo(() => { @@ -12,21 +13,23 @@ export const ShowMessage: React.FC<{ message: MessageRow }> = ({ message }) => { return (
-
-

{message.type === 'log' ? 'Message' : 'HTTP'} Details

-
- -
-
-
Timestamp
-
{createdAt}
+
+
+

{message.type === 'log' ? 'Message' : 'HTTP'} Details

-
-
Status
-
+
+
+
 
+
+ +
{createdAt}
+
+
+ +
Source
@@ -35,11 +38,7 @@ export const ShowMessage: React.FC<{ message: MessageRow }> = ({ message }) => {
-

Message

-
{message.message}
-
-
-

Error

+

Payload

= ({ message }) => { return { code: { padding: '0', whiteSpace: 'pre-wrap' } }; }} > - {JSON.stringify(message.error, null, 2)} + {JSON.stringify({ message: message.message, error: message.error || undefined }, null, 2)}
diff --git a/packages/webapp/src/pages/Logs/ShowOperation.tsx b/packages/webapp/src/pages/Logs/ShowOperation.tsx index d1c70c2502..1c682fafb1 100644 --- a/packages/webapp/src/pages/Logs/ShowOperation.tsx +++ b/packages/webapp/src/pages/Logs/ShowOperation.tsx @@ -6,7 +6,7 @@ import { OperationTag } from './components/OperationTag'; import { StatusTag } from './components/StatusTag'; import { elapsedTime, formatDateToLogFormat } from '../../utils/utils'; import { Link } from 'react-router-dom'; -import { ExternalLinkIcon } from '@radix-ui/react-icons'; +import { CalendarIcon, ClockIcon, ExternalLinkIcon } from '@radix-ui/react-icons'; import { SearchInOperation } from './components/SearchInOperation'; import { Skeleton } from '../../components/ui/Skeleton'; @@ -58,22 +58,41 @@ export const ShowOperation: React.FC<{ operationId: string }> = ({ operationId } return (
-
-

Operation Details

-
+
+

Operation Details

+
+
+ +
+
 
+
+ +
{duration}
+
+
 
+
+ +
{createdAt}
+
+
+
-
+
-
Timestamp
-
{createdAt}
+
Type
+
+ +
-
+
+
+
Integration
{operation.configName ? ( - +
{operation.configName}
-
+
@@ -82,13 +101,14 @@ export const ShowOperation: React.FC<{ operationId: string }> = ({ operationId } )}
-
+
 
+
Connection
{operation.connectionName ? ( - +
{operation.connectionName}
-
+
@@ -97,25 +117,10 @@ export const ShowOperation: React.FC<{ operationId: string }> = ({ operationId } )}
-
-
Duration
-
{duration}
-
-
-
Type
-
- -
-
-
+
 
+
Script
-
{operation.syncName ? operation.syncName : 'n/a'}
-
-
-
Status
-
- -
+
{operation.syncName ? operation.syncName : 'n/a'}
diff --git a/packages/webapp/src/pages/Logs/components/OperationRow.tsx b/packages/webapp/src/pages/Logs/components/OperationRow.tsx index 36f3a03143..bfd11018f6 100644 --- a/packages/webapp/src/pages/Logs/components/OperationRow.tsx +++ b/packages/webapp/src/pages/Logs/components/OperationRow.tsx @@ -20,7 +20,7 @@ export const OperationRow: React.FC<{ row: Row }> = ({ row
-
+
diff --git a/packages/webapp/src/pages/Logs/components/OperationTag.tsx b/packages/webapp/src/pages/Logs/components/OperationTag.tsx index 3451792628..55873b132e 100644 --- a/packages/webapp/src/pages/Logs/components/OperationTag.tsx +++ b/packages/webapp/src/pages/Logs/components/OperationTag.tsx @@ -3,22 +3,32 @@ import type { SearchOperationsData } from '@nangohq/types'; import { cn } from '../../../utils/utils'; import { Tag } from './Tag'; import { CrossCircledIcon, LoopIcon, PauseIcon, PlayIcon, ResumeIcon, UploadIcon } from '@radix-ui/react-icons'; +import * as Tooltip from '../../../components/ui/Tooltip'; export const OperationTag: React.FC<{ operation: Exclude; highlight?: boolean }> = ({ operation, highlight }) => { if (operation.type === 'sync') { return (
- + {operation.type} - - {operation.action === 'cancel' && } - {operation.action === 'init' && } - {operation.action === 'pause' && } - {operation.action === 'run' && } - {operation.action === 'run_full' && } - {operation.action === 'unpause' && } - + + + + {operation.action === 'cancel' && } + {operation.action === 'init' && } + {operation.action === 'pause' && } + {operation.action === 'run' && } + {operation.action === 'run_full' && } + {operation.action === 'unpause' && } + + + +

+ {operation.action} {operation.type} +

+
+
); } diff --git a/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx index 52bb1648c3..5ff653c276 100644 --- a/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx +++ b/packages/webapp/src/pages/Logs/components/SearchInOperation.tsx @@ -3,7 +3,7 @@ import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-tabl import { Input } from '../../../components/ui/input/Input'; import { useSearchMessages } from '../../../hooks/useLogs'; import type { SearchOperationsData } from '@nangohq/types'; -import { formatDateToLogFormat } from '../../../utils/utils'; +import { formatDateToLogFormat, formatQuantity } from '../../../utils/utils'; import { useStore } from '../../../store'; import * as Table from '../../../components/ui/Table'; import Spinner from '../../../components/ui/Spinner'; @@ -11,7 +11,7 @@ import Info from '../../../components/ui/Info'; import { LevelTag } from './LevelTag'; import { MessageRow } from './MessageRow'; import { ChevronRightIcon, MagnifyingGlassIcon } from '@radix-ui/react-icons'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useDebounce } from 'react-use'; import { Tag } from './Tag'; @@ -76,9 +76,19 @@ export const SearchInOperation: React.FC<{ operationId: string }> = ({ operation }); useDebounce(() => setDebouncedSearch(search), 250, [search]); + const total = useMemo(() => { + if (!data?.pagination) { + return 0; + } + return formatQuantity(data.pagination.total); + }, [data?.pagination]); + return (
-

Logs {loading && }

+
+

Logs {loading && }

+
{total} logs found
+
} diff --git a/packages/webapp/src/utils/utils.tsx b/packages/webapp/src/utils/utils.tsx index bdbaa0822c..4048d4076b 100644 --- a/packages/webapp/src/utils/utils.tsx +++ b/packages/webapp/src/utils/utils.tsx @@ -373,3 +373,8 @@ export function parseEndpoint(endpoint: string | FlowEndpoint): string { export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +const quantityFormatter = Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1, minimumFractionDigits: 0 }); +export function formatQuantity(quantity: number): string { + return quantityFormatter.format(quantity); +} From 4703e3088993d9b89a59e5cb08e416802df75028 Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Tue, 21 May 2024 16:58:27 +0200 Subject: [PATCH 10/14] console.log --- packages/logs/lib/models/messages.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/logs/lib/models/messages.ts b/packages/logs/lib/models/messages.ts index b54940bcc2..0f64cc035c 100644 --- a/packages/logs/lib/models/messages.ts +++ b/packages/logs/lib/models/messages.ts @@ -169,8 +169,6 @@ export async function listMessages(opts: { }); } - console.log(JSON.stringify(query)); - const res = await client.search<{ hits: { total: number; hits: { _source: MessageRow }[] } }>({ index: indexMessages.index, size: opts.limit, From 78938e9363fc03f526e023fd555fe42394e75ff8 Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Wed, 22 May 2024 09:39:07 +0200 Subject: [PATCH 11/14] review --- packages/logs/lib/models/helpers.ts | 4 +++- packages/server/lib/controllers/v1/logs/getOperation.ts | 2 +- packages/server/lib/controllers/v1/logs/searchMessages.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/logs/lib/models/helpers.ts b/packages/logs/lib/models/helpers.ts index 656642c8b9..16564045ab 100644 --- a/packages/logs/lib/models/helpers.ts +++ b/packages/logs/lib/models/helpers.ts @@ -1,7 +1,9 @@ import { nanoid } from '@nangohq/utils'; import type { MessageRow } from '@nangohq/types'; +import { z } from 'zod'; + +export const operationIdRegex = z.string().regex(/([0-9]|[a-zA-Z0-9]{20})/); -export const operationIdRegex = /([0-9]|[a-zA-Z0-9]{20})/; export interface FormatMessageData { account?: { id: number; name?: string }; user?: { id: number } | undefined; diff --git a/packages/server/lib/controllers/v1/logs/getOperation.ts b/packages/server/lib/controllers/v1/logs/getOperation.ts index 7b896edb67..7748ac3ea2 100644 --- a/packages/server/lib/controllers/v1/logs/getOperation.ts +++ b/packages/server/lib/controllers/v1/logs/getOperation.ts @@ -6,7 +6,7 @@ import { model, envs, operationIdRegex } from '@nangohq/logs'; const validation = z .object({ - operationId: z.string().regex(operationIdRegex) + operationId: operationIdRegex }) .strict(); diff --git a/packages/server/lib/controllers/v1/logs/searchMessages.ts b/packages/server/lib/controllers/v1/logs/searchMessages.ts index 8f731da517..3e5c457977 100644 --- a/packages/server/lib/controllers/v1/logs/searchMessages.ts +++ b/packages/server/lib/controllers/v1/logs/searchMessages.ts @@ -6,7 +6,7 @@ import { requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; const validation = z .object({ - operationId: z.string().regex(operationIdRegex), + operationId: operationIdRegex, limit: z.number().optional().default(100), search: z.string().optional(), states: z From 89bbf2bfaaa10b7afb43ad964a1073b0298fd4c9 Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Wed, 22 May 2024 11:17:27 +0200 Subject: [PATCH 12/14] review --- README.md | 7 +++---- docs-v2/integrations/overview.mdx | 4 ++-- packages/webapp/src/hooks/useLogs.tsx | 1 - packages/webapp/src/pages/Logs/ShowOperation.tsx | 6 +++++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4ad4aaa5ca..71aafe66d6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- +
@@ -29,7 +29,7 @@ Nango is a single API to interact with all other external APIs. It should be the -## 👩‍💻 Sample code +## 👩‍💻 Sample code Initiate a new OAuth flow from your frontend: @@ -58,7 +58,6 @@ Nango's flexibility ensures it supports any API integration: # 🔌 190+ pre-built APIs & integrations, or build your own! [Over 190 APIs are pre-configured](https://nango.dev/integrations) to work right out of the box. We support 25+ categories such: - - **Accounting**: Netsuite, Quickbooks, Xero, ... - **Communications**: Slack, Discord, Teams, ... - **CRMs**: Hubspot, Salesforce, ... @@ -84,7 +83,6 @@ Sign up for free and try the interactive demo: # 🙋‍♀️ Why is Nango open-source? Our mission is to enable all SaaS to seamlessly integrate together. By being open source, every engineer can contribute improvements to the platform for everyone: - - [Contribute an API](/customize/guides/contribute-an-api) - [Contribute integration templates](/customize/guides/contribute-an-integration-template) @@ -105,3 +103,4 @@ Thank you for continuously making Nango better ❤️ # 🐻 History Pizzly (a simple service for OAuth) was initially developed by the team at [Bearer](https://www.bearer.com/?ref=pizzly) with contributions of more than 40 individuals. Over time the focus of Bearer shifted and they could no longer maintain Pizzly. In late 2022 the team at [Nango](https://www.nango.dev) adopted the project and has since maintained and evolved it together with the growing Nango community. + diff --git a/docs-v2/integrations/overview.mdx b/docs-v2/integrations/overview.mdx index 2eb9eff1a4..78e47ce862 100644 --- a/docs-v2/integrations/overview.mdx +++ b/docs-v2/integrations/overview.mdx @@ -7,7 +7,7 @@ sidebarTitle: Overview Nango is an open-source platform for product integrations. Integrate with external APIs in hours, not weeks, while maintaining full control ([learn more](/introduction)). -Nango is pre-configured for 190+ APIs and integration templates. +Nango is pre-configured for 190+ APIs and integration templates. # Explore APIs by Category @@ -90,4 +90,4 @@ Nango is pre-configured for 190+ APIs and integration templates. Missing an API or integration template? - request it in the [community](https://nango.dev/slack); we deliver them in <48h -- contribute it; it's fast & easy ([new API](/customize/guides/contribute-an-integration-template) / [new integration template](/customize/guides/contribute-an-integration-template)) +- contribute it; it's fast & easy ([new API](/customize/guides/contribute-an-integration-template) / [new integration template](/customize/guides/contribute-an-integration-template)) \ No newline at end of file diff --git a/packages/webapp/src/hooks/useLogs.tsx b/packages/webapp/src/hooks/useLogs.tsx index e37d66cfe8..e20b726fd4 100644 --- a/packages/webapp/src/hooks/useLogs.tsx +++ b/packages/webapp/src/hooks/useLogs.tsx @@ -25,7 +25,6 @@ export function useSearchOperations(env: string, body: SearchOperations['Body']) setError(undefined); setData((await res.json()) as SearchOperations['Success']); } catch (err) { - console.log(err); setData(undefined); setError(err as any); } finally { diff --git a/packages/webapp/src/pages/Logs/ShowOperation.tsx b/packages/webapp/src/pages/Logs/ShowOperation.tsx index 1c682fafb1..33e31a9c14 100644 --- a/packages/webapp/src/pages/Logs/ShowOperation.tsx +++ b/packages/webapp/src/pages/Logs/ShowOperation.tsx @@ -106,7 +106,11 @@ export const ShowOperation: React.FC<{ operationId: string }> = ({ operationId }
Connection
{operation.connectionName ? ( - +
{operation.connectionName}
From b6c83ed15ba7096c225443d02726f950e97de4f3 Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Wed, 22 May 2024 11:19:04 +0200 Subject: [PATCH 13/14] default height --- packages/webapp/src/components/ui/Skeleton.tsx | 2 +- packages/webapp/src/pages/Logs/ShowOperation.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/webapp/src/components/ui/Skeleton.tsx b/packages/webapp/src/components/ui/Skeleton.tsx index a7c3f9afa0..71a2f0cad1 100644 --- a/packages/webapp/src/components/ui/Skeleton.tsx +++ b/packages/webapp/src/components/ui/Skeleton.tsx @@ -1,7 +1,7 @@ import { cn } from '../../utils/utils'; function Skeleton({ className, ...props }: React.HTMLAttributes) { - return
; + return
; } export { Skeleton }; diff --git a/packages/webapp/src/pages/Logs/ShowOperation.tsx b/packages/webapp/src/pages/Logs/ShowOperation.tsx index 33e31a9c14..b0238e3627 100644 --- a/packages/webapp/src/pages/Logs/ShowOperation.tsx +++ b/packages/webapp/src/pages/Logs/ShowOperation.tsx @@ -32,15 +32,15 @@ export const ShowOperation: React.FC<{ operationId: string }> = ({ operationId } return (

Operation Details

- - + +

Payload

- +

Logs

- +
); From 319fa91ca9e68feb0500a1d7cf94c89eb8f202c0 Mon Sep 17 00:00:00 2001 From: Samuel Bodin <1637651+bodinsamuel@users.noreply.github.com> Date: Wed, 22 May 2024 11:25:17 +0200 Subject: [PATCH 14/14] console .log --- packages/webapp/src/hooks/useLogs.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/webapp/src/hooks/useLogs.tsx b/packages/webapp/src/hooks/useLogs.tsx index e20b726fd4..9c0281bf88 100644 --- a/packages/webapp/src/hooks/useLogs.tsx +++ b/packages/webapp/src/hooks/useLogs.tsx @@ -75,7 +75,6 @@ export function useSearchMessages(env: string, body: SearchMessages['Body']) { setError(undefined); setData((await res.json()) as SearchMessages['Success']); } catch (err) { - console.log(err); setData(undefined); setError(err as any); } finally {