Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add tanstack query #2406

Merged
merged 2 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,021 changes: 961 additions & 60 deletions frontend/package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
"@headlessui/react": "2.1.2",
"@heroicons/react": "2.1.5",
"@tailwindcss/forms": "^0.5.6",
"@tanstack/react-query": "^5.51.23",
"@tanstack/react-query-devtools": "^5.51.23",
"@uiw/codemirror-theme-atomone": "^4.22.0",
"@uiw/codemirror-theme-github": "^4.22.0",
"@vitejs/plugin-react": "^4.0.4",
Expand Down Expand Up @@ -59,6 +61,7 @@
"@storybook/react": "^8.2.7",
"@storybook/react-vite": "^8.2.7",
"@storybook/test": "^8.2.7",
"@tanstack/eslint-plugin-query": "^5.51.15",
"@types/react": "18.3.3",
"@types/react-dom": "18.3.0",
"buffer": "^6.0.3",
Expand Down
24 changes: 2 additions & 22 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,5 @@
import { Navigate, Route, Routes } from 'react-router-dom'
import { ConsolePage } from './features/console/ConsolePage.tsx'
import { DeploymentPage } from './features/deployments/DeploymentPage.tsx'
import { DeploymentsPage } from './features/deployments/DeploymentsPage.tsx'
import { TimelinePage } from './features/timeline/TimelinePage.tsx'
import { VerbPage } from './features/verbs/VerbPage.tsx'
import { Layout } from './layout/Layout.tsx'
import { NotFoundPage } from './layout/NotFoundPage.tsx'
import { AppProvider } from './providers/app-providers.tsx'

export const App = () => {
return (
<Routes>
<Route path='/' element={<Layout />}>
<Route path='/' element={<Navigate to='events' replace />} />
<Route path='events' element={<TimelinePage />} />

<Route path='deployments' element={<DeploymentsPage />} />
<Route path='deployments/:deploymentKey' element={<DeploymentPage />} />
<Route path='deployments/:deploymentKey/verbs/:verbName' element={<VerbPage />} />
<Route path='console' element={<ConsolePage />} />
</Route>
<Route path='*' element={<NotFoundPage />} />
</Routes>
)
return <AppProvider />
}
45 changes: 45 additions & 0 deletions frontend/src/api/modules/use-modules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Code, ConnectError } from '@connectrpc/connect'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect } from 'react'
import { useClient } from '../../hooks/use-client'
import { ConsoleService } from '../../protos/xyz/block/ftl/v1/console/console_connect'
import { useSchema } from '../schema/use-schema'

const useModulesKey = 'modules'

export const useModules = () => {
const client = useClient(ConsoleService)
const queryClient = useQueryClient()
const { data: streamingData } = useSchema()

useEffect(() => {
if (streamingData) {
queryClient.invalidateQueries({
queryKey: [useModulesKey],
})
}
}, [streamingData, queryClient])

const fetchModules = async (signal: AbortSignal) => {
try {
console.debug('fetching modules from FTL')
const modules = await client.getModules({}, { signal })
return modules ?? []
} catch (error) {
if (error instanceof ConnectError) {
if (error.code !== Code.Canceled) {
console.error('fetchModules - Connect error:', error)
}
} else {
console.error('fetchModules:', error)
}
throw error
}
}

return useQuery({
queryKey: [useModulesKey],
queryFn: async ({ signal }) => fetchModules(signal),
enabled: !!streamingData,
})
}
53 changes: 53 additions & 0 deletions frontend/src/api/schema/use-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Code, ConnectError } from '@connectrpc/connect'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useClient } from '../../hooks/use-client.ts'
import { useVisibility } from '../../hooks/use-visibility.ts'
import { ControllerService } from '../../protos/xyz/block/ftl/v1/ftl_connect.ts'
import { DeploymentChangeType, type PullSchemaResponse } from '../../protos/xyz/block/ftl/v1/ftl_pb.ts'

const streamingSchemaKey = 'streamingSchema'

export const useSchema = () => {
const client = useClient(ControllerService)
const queryClient = useQueryClient()
const isVisible = useVisibility()

const streamSchema = async (signal: AbortSignal) => {
try {
const schemaMap = new Map<string, PullSchemaResponse>()
for await (const response of client.pullSchema({}, { signal })) {
const moduleName = response.moduleName ?? ''
console.log(`schema changed: ${DeploymentChangeType[response.changeType]} ${moduleName}`)
switch (response.changeType) {
case DeploymentChangeType.DEPLOYMENT_ADDED:
schemaMap.set(moduleName, response)
break
case DeploymentChangeType.DEPLOYMENT_CHANGED:
schemaMap.set(moduleName, response)
break
case DeploymentChangeType.DEPLOYMENT_REMOVED:
schemaMap.delete(moduleName)
}

if (!response.more) {
const schema = Array.from(schemaMap.values()).sort((a, b) => a.schema?.name?.localeCompare(b.schema?.name ?? '') ?? 0)
queryClient.setQueryData([streamingSchemaKey], schema)
}
}
} catch (error) {
if (error instanceof ConnectError) {
if (error.code !== Code.Canceled) {
console.error('useSchema - streamSchema - Connect error:', error)
}
} else {
console.error('useSchema - streamSchema:', error)
}
}
}

return useQuery({
queryKey: [streamingSchemaKey],
queryFn: async ({ signal }) => streamSchema(signal),
enabled: isVisible,
})
}
5 changes: 5 additions & 0 deletions frontend/src/api/timeline/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './stream-verb-calls'
export * from './timeline-filters'
export * from './use-request-calls'
export * from './use-timeline-calls'
export * from './use-timeline'
6 changes: 6 additions & 0 deletions frontend/src/api/timeline/stream-verb-calls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { callFilter } from './timeline-filters.ts'
import { useTimelineCalls } from './use-timeline-calls.ts'

export const useStreamVerbCalls = (moduleName?: string, verbName?: string, enabled = true) => {
return useTimelineCalls(true, [callFilter(moduleName || '', verbName)], enabled)
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
import type { Timestamp } from '@bufbuild/protobuf'
import { Code, ConnectError } from '@connectrpc/connect'
import { createClient } from '../hooks/use-client'
import { ConsoleService } from '../protos/xyz/block/ftl/v1/console/console_connect'
import {
type CallEvent,
type Event,
EventType,
type EventType,
EventsQuery_CallFilter,
EventsQuery_DeploymentFilter,
EventsQuery_EventTypeFilter,
EventsQuery_Filter,
EventsQuery_IDFilter,
EventsQuery_LogLevelFilter,
EventsQuery_Order,
EventsQuery_RequestFilter,
EventsQuery_TimeFilter,
type LogLevel,
} from '../protos/xyz/block/ftl/v1/console/console_pb'

const client = createClient(ConsoleService)
} from '../../protos/xyz/block/ftl/v1/console/console_pb'

export const requestKeysFilter = (requestKeys: string[]): EventsQuery_Filter => {
const filter = new EventsQuery_Filter()
Expand Down Expand Up @@ -106,88 +98,3 @@ export const eventIdFilter = ({
}
return filter
}

export const getRequestCalls = async ({
abortControllerSignal,
requestKey,
}: {
abortControllerSignal: AbortSignal
requestKey: string
}): Promise<CallEvent[]> => {
const allEvents = await getEvents({
abortControllerSignal,
filters: [requestKeysFilter([requestKey]), eventTypesFilter([EventType.CALL])],
})
return allEvents.map((e) => e.entry.value) as CallEvent[]
}

export const getCalls = async ({
abortControllerSignal,
destModule,
destVerb,
sourceModule,
}: {
abortControllerSignal: AbortSignal
destModule: string
destVerb?: string
sourceModule?: string
}): Promise<CallEvent[]> => {
const allEvents = await getEvents({
abortControllerSignal,
filters: [callFilter(destModule, destVerb, sourceModule), eventTypesFilter([EventType.CALL])],
})
return allEvents.map((e) => e.entry.value) as CallEvent[]
}

export const getEvents = async ({
abortControllerSignal,
limit = 1000,
order = EventsQuery_Order.DESC,
filters = [],
}: {
abortControllerSignal: AbortSignal
limit?: number
order?: EventsQuery_Order
filters?: EventsQuery_Filter[]
}): Promise<Event[]> => {
try {
const response = await client.getEvents({ filters, limit, order }, { signal: abortControllerSignal })
return response.events
} catch (error) {
if (error instanceof ConnectError) {
if (error.code === Code.Canceled) {
return []
}
}
throw error
}
}

export const streamEvents = async ({
abortControllerSignal,
filters,
onEventsReceived,
}: {
abortControllerSignal: AbortSignal
filters: EventsQuery_Filter[]
onEventsReceived: (events: Event[]) => void
}) => {
try {
for await (const response of client.streamEvents(
{ updateInterval: { seconds: BigInt(1) }, query: { limit: 200, filters, order: EventsQuery_Order.DESC } },
{ signal: abortControllerSignal },
)) {
if (response.events) {
onEventsReceived(response.events)
}
}
} catch (error) {
if (error instanceof ConnectError) {
if (error.code !== Code.Canceled) {
console.error('Console service - streamEvents - Connect error:', error)
}
} else {
console.error('Console service - streamEvents:', error)
}
}
}
6 changes: 6 additions & 0 deletions frontend/src/api/timeline/use-request-calls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { requestKeysFilter } from './timeline-filters'
import { useTimelineCalls } from './use-timeline-calls'

export const useRequestCalls = (requestKey?: string) => {
return useTimelineCalls(true, [requestKeysFilter([requestKey || ''])], !!requestKey)
}
16 changes: 16 additions & 0 deletions frontend/src/api/timeline/use-timeline-calls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { type CallEvent, EventType, type EventsQuery_Filter } from '../../protos/xyz/block/ftl/v1/console/console_pb.ts'
import { eventTypesFilter } from './timeline-filters.ts'
import { useTimeline } from './use-timeline.ts'

export const useTimelineCalls = (isStreaming: boolean, filters: EventsQuery_Filter[], enabled = true) => {
const allFilters = [...filters, eventTypesFilter([EventType.CALL])]
const timelineQuery = useTimeline(isStreaming, allFilters, enabled)

// Map the events to CallEvent for ease of use
const data = timelineQuery.data?.map((event) => event.entry.value as CallEvent) || []

return {
...timelineQuery,
data,
}
}
63 changes: 63 additions & 0 deletions frontend/src/api/timeline/use-timeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Code, ConnectError } from '@connectrpc/connect'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useClient } from '../../hooks/use-client'
import { useVisibility } from '../../hooks/use-visibility'
import { ConsoleService } from '../../protos/xyz/block/ftl/v1/console/console_connect'
import { type EventsQuery_Filter, EventsQuery_Order } from '../../protos/xyz/block/ftl/v1/console/console_pb'

const timelineKey = 'timeline'
const maxTimelineEntries = 1000

export const useTimeline = (isStreaming: boolean, filters: EventsQuery_Filter[], enabled = true) => {
const client = useClient(ConsoleService)
const queryClient = useQueryClient()
const isVisible = useVisibility()

const order = EventsQuery_Order.DESC
const limit = isStreaming ? 200 : 1000

const queryKey = [timelineKey, isStreaming, filters, order, limit]

const fetchTimeline = async ({ signal }: { signal: AbortSignal }) => {
try {
console.log('fetching timeline')
const response = await client.getEvents({ filters, limit, order }, { signal })
return response.events
} catch (error) {
if (error instanceof ConnectError) {
if (error.code === Code.Canceled) {
return []
}
}
throw error
}
}

const streamTimeline = async ({ signal }: { signal: AbortSignal }) => {
try {
console.log('streaming timeline')
console.log('filters:', filters)
for await (const response of client.streamEvents({ updateInterval: { seconds: BigInt(1) }, query: { limit, filters, order } }, { signal })) {
if (response.events) {
const prev = queryClient.getQueryData<Event[]>(queryKey) ?? []
const allEvents = [...response.events, ...prev].slice(0, maxTimelineEntries)
queryClient.setQueryData(queryKey, allEvents)
}
}
} catch (error) {
if (error instanceof ConnectError) {
if (error.code !== Code.Canceled) {
console.error('Console service - streamEvents - Connect error:', error)
}
} else {
console.error('Console service - streamEvents:', error)
}
}
}

return useQuery({
queryKey: queryKey,
queryFn: async ({ signal }) => (isStreaming ? streamTimeline({ signal }) : fetchTimeline({ signal })),
enabled: enabled && isVisible,
})
}
4 changes: 2 additions & 2 deletions frontend/src/components/CodeEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { defaultKeymap } from '@codemirror/commands'
import { handleRefresh, jsonSchemaHover, jsonSchemaLinter, stateExtensions } from 'codemirror-json-schema'
import { json5, json5ParseLinter } from 'codemirror-json5'
import { useCallback, useEffect, useRef } from 'react'
import { useDarkMode } from '../providers/dark-mode-provider'
import { useUserPreferences } from '../providers/user-preferences-provider'

const commonExtensions = [
gutter({ class: 'CodeMirror-lint-markers' }),
Expand All @@ -33,7 +33,7 @@ export interface InitialState {
}

export const CodeEditor = ({ initialState, onTextChanged }: { initialState: InitialState; onTextChanged?: (text: string) => void }) => {
const { isDarkMode } = useDarkMode()
const { isDarkMode } = useUserPreferences()
const editorContainerRef = useRef(null)
const editorViewRef = useRef<EditorView | null>(null)

Expand Down
Loading
Loading