diff --git a/console/client/package-lock.json b/console/client/package-lock.json index fa2d7a76c1..7bf35c1f71 100644 --- a/console/client/package-lock.json +++ b/console/client/package-lock.json @@ -8,9 +8,8 @@ "name": "console", "version": "0.0.0", "dependencies": { - "@bufbuild/connect": "0.12.0", - "@bufbuild/connect-web": "0.12.0", - "@bufbuild/protobuf": "1.3.0", + "@connectrpc/connect": "^1.1.2", + "@connectrpc/connect-web": "^1.1.2", "@headlessui/react": "1.7.16", "@heroicons/react": "2.0.18", "@monaco-editor/react": "4.5.2", @@ -31,9 +30,6 @@ "vite": "^4.4.9" }, "devDependencies": { - "@bufbuild/buf": "1.26.1", - "@bufbuild/protoc-gen-connect-es": "0.12.0", - "@bufbuild/protoc-gen-es": "1.3.0", "@jest/globals": "29.6.2", "@swc/core": "1.3.77", "@swc/jest": "0.2.29", @@ -760,142 +756,27 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@bufbuild/buf": { - "version": "1.26.1", - "resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.26.1.tgz", - "integrity": "sha512-NyYx4T//3ndtFYV3BfqX9Xrm1NZEx3eChXniAKc/osCVViFooC5nuLQUbyqglMonH0w39RohiURMXN+e/oEB4g==", - "dev": true, - "hasInstallScript": true, - "bin": { - "buf": "bin/buf", - "protoc-gen-buf-breaking": "bin/protoc-gen-buf-breaking", - "protoc-gen-buf-lint": "bin/protoc-gen-buf-lint" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@bufbuild/buf-darwin-arm64": "1.26.1", - "@bufbuild/buf-darwin-x64": "1.26.1", - "@bufbuild/buf-linux-aarch64": "1.26.1", - "@bufbuild/buf-linux-x64": "1.26.1", - "@bufbuild/buf-win32-arm64": "1.26.1", - "@bufbuild/buf-win32-x64": "1.26.1" - } - }, - "node_modules/@bufbuild/buf-darwin-arm64": { - "version": "1.26.1", - "resolved": "https://registry.npmjs.org/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.26.1.tgz", - "integrity": "sha512-nmyWiT/59RFja0ZuXFxjNGoAMDPTApU66CZUUevkFVWbNB9nzeQDjx2vsJyACY64k5fTgZiaelSiyppwObQknw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@bufbuild/connect": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@bufbuild/connect/-/connect-0.12.0.tgz", - "integrity": "sha512-rYn3Akp7teOkvqxguLbf6QKizH37Yeo33lseV+JHDZC19CsAV9wrrZM17Sa+LNEBj/hrYtQF7EIcllgGxhv9aw==", - "peerDependencies": { - "@bufbuild/protobuf": "^1.2.1" - } - }, - "node_modules/@bufbuild/connect-web": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@bufbuild/connect-web/-/connect-web-0.12.0.tgz", - "integrity": "sha512-bw5E6aIs0q3ab5oyIB263ShRg8DhUMsewMHsl/9lUxJFkmzxvHoPIlNFrSWOUUcyudJrahfhDveXF94bi9C5cA==", - "dependencies": { - "@bufbuild/connect": "0.12.0" - }, - "peerDependencies": { - "@bufbuild/protobuf": "^1.2.1" - } - }, "node_modules/@bufbuild/protobuf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.3.0.tgz", - "integrity": "sha512-G372ods0pLt46yxVRsnP/e2btVPuuzArcMPFpIDeIwiGPuuglEs9y75iG0HMvZgncsj5TvbYRWqbVyOe3PLCWQ==" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.3.3.tgz", + "integrity": "sha512-AoHSiIpTFF97SQgmQni4c+Tyr0CDhkaRaR2qGEJTEbauqQwLRpLrd9yVv//wVHOSxr/b4FJcL54VchhY6710xA==", + "peer": true }, - "node_modules/@bufbuild/protoc-gen-connect-es": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-connect-es/-/protoc-gen-connect-es-0.12.0.tgz", - "integrity": "sha512-J7/9oF/ByAQrZmEZkhNRnzo56PK+KOUMNJGxQGhF9Mjrrr7q/eCkd5tuao4Yk+A2biDWLk84+L6Zl8uHiBMK3w==", - "dev": true, - "dependencies": { - "@bufbuild/protobuf": "^1.2.1", - "@bufbuild/protoplugin": "^1.2.1" - }, - "bin": { - "protoc-gen-connect-es": "bin/protoc-gen-connect-es" - }, - "engines": { - "node": ">=16.0.0" - }, + "node_modules/@connectrpc/connect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.1.2.tgz", + "integrity": "sha512-oDuKJFRORtzyH4IhZyNgIQ5DKjlDnbP72AH55Aabpc0fwApyus/h4cmYU1KDvahVbqsvUOpd5qUTyMH8IhMmLA==", "peerDependencies": { - "@bufbuild/connect": "0.12.0", - "@bufbuild/protoc-gen-es": "^1.2.1" - }, - "peerDependenciesMeta": { - "@bufbuild/connect": { - "optional": true - }, - "@bufbuild/protoc-gen-es": { - "optional": true - } + "@bufbuild/protobuf": "^1.3.3" } }, - "node_modules/@bufbuild/protoc-gen-es": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@bufbuild/protoc-gen-es/-/protoc-gen-es-1.3.0.tgz", - "integrity": "sha512-XxGZwpXMYlwoSyJwCTFb7SZ2xKmv2iCRM022t1wszhY3kNL7rjpyj+3GbpCOjaM1T7NAoLnW0Hyb/M0b0XDb3Q==", - "dev": true, - "dependencies": { - "@bufbuild/protoplugin": "1.3.0" - }, - "bin": { - "protoc-gen-es": "bin/protoc-gen-es" - }, - "engines": { - "node": ">=14" - }, + "node_modules/@connectrpc/connect-web": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.1.2.tgz", + "integrity": "sha512-6Osvp4d/5Qvf0dsbUmqgzCPFIong9KBm5G24g2gapPW2huAtyVj+KwdG6453EKCirPZ5qZHY0FywLef57op9YQ==", "peerDependencies": { - "@bufbuild/protobuf": "1.3.0" - }, - "peerDependenciesMeta": { - "@bufbuild/protobuf": { - "optional": true - } - } - }, - "node_modules/@bufbuild/protoplugin": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@bufbuild/protoplugin/-/protoplugin-1.3.0.tgz", - "integrity": "sha512-zye8CfJb9VWzaHR/f1qcEkddaRh9De+u6fORsj92Ten8EJUcyhiY5BivET+RMTissAKXKrp/f2zSBCV0dlFxPw==", - "dev": true, - "dependencies": { - "@bufbuild/protobuf": "1.3.0", - "@typescript/vfs": "^1.4.0", - "typescript": "4.5.2" - } - }, - "node_modules/@bufbuild/protoplugin/node_modules/typescript": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", - "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" + "@bufbuild/protobuf": "^1.3.3", + "@connectrpc/connect": "1.1.2" } }, "node_modules/@csstools/selector-specificity": { @@ -3047,15 +2928,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript/vfs": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.5.0.tgz", - "integrity": "sha512-AJS307bPgbsZZ9ggCT3wwpg3VbTKMFNHfaY/uF0ahSkYYrPF2dSSKDNIDIQAHm9qJqbLvCsSJH7yN4Vs/CsMMg==", - "dev": true, - "dependencies": { - "debug": "^4.1.1" - } - }, "node_modules/@vitejs/plugin-react": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.0.4.tgz", diff --git a/console/client/package.json b/console/client/package.json index e0297fa4ec..29a14c9b2a 100644 --- a/console/client/package.json +++ b/console/client/package.json @@ -26,9 +26,8 @@ ], "source": "index.html", "dependencies": { - "@bufbuild/connect": "0.12.0", - "@bufbuild/connect-web": "0.12.0", - "@bufbuild/protobuf": "1.3.0", + "@connectrpc/connect": "^1.1.2", + "@connectrpc/connect-web": "^1.1.2", "@headlessui/react": "1.7.16", "@heroicons/react": "2.0.18", "@monaco-editor/react": "4.5.2", @@ -49,9 +48,6 @@ "vite": "^4.4.9" }, "devDependencies": { - "@bufbuild/buf": "1.26.1", - "@bufbuild/protoc-gen-connect-es": "0.12.0", - "@bufbuild/protoc-gen-es": "1.3.0", "@jest/globals": "29.6.2", "@swc/core": "1.3.77", "@swc/jest": "0.2.29", diff --git a/console/client/src/features/timeline/Timeline.tsx b/console/client/src/features/timeline/Timeline.tsx index 0b59a18072..89f7727c1b 100644 --- a/console/client/src/features/timeline/Timeline.tsx +++ b/console/client/src/features/timeline/Timeline.tsx @@ -1,6 +1,7 @@ import { Timestamp } from '@bufbuild/protobuf' import { useContext, useEffect, useState } from 'react' import { useSearchParams } from 'react-router-dom' +import { useVisibility } from '../../hooks/use-visibility.ts' import { Event, EventsQuery_Filter } from '../../protos/xyz/block/ftl/v1/console/console_pb.ts' import { SidePanelContext } from '../../providers/side-panel-provider.tsx' import { eventIdFilter, getEvents, streamEvents, timeFilter } from '../../services/console.service.ts' @@ -25,10 +26,15 @@ export const Timeline = ({ timeSettings, filters }: { timeSettings: TimeSettings const { openPanel, closePanel, isOpen } = useContext(SidePanelContext) const [entries, setEntries] = useState([]) const [selectedEntry, setSelectedEntry] = useState(null) + const isVisible = useVisibility() useEffect(() => { const eventId = searchParams.get('id') const abortController = new AbortController() + if (!isVisible) { + abortController.abort() + return + } const fetchEvents = async () => { let eventFilters = filters @@ -56,9 +62,9 @@ export const Timeline = ({ timeSettings, filters }: { timeSettings: TimeSettings streamEvents({ abortControllerSignal: abortController.signal, filters, - onEventReceived: (event) => { + onEventsReceived: (events) => { if (!timeSettings.isPaused) { - setEntries((prev) => [event, ...prev].slice(0, maxTimelineEntries)) + setEntries((prev) => [...events, ...prev].slice(0, maxTimelineEntries)) } }, }) @@ -68,7 +74,7 @@ export const Timeline = ({ timeSettings, filters }: { timeSettings: TimeSettings return () => { abortController.abort() } - }, [filters, timeSettings]) + }, [filters, timeSettings, isVisible]) useEffect(() => { if (!isOpen) { diff --git a/console/client/src/features/timeline/filters/TimelineFilterPanel.tsx b/console/client/src/features/timeline/filters/TimelineFilterPanel.tsx index 61ecfcf99e..4fe1052b92 100644 --- a/console/client/src/features/timeline/filters/TimelineFilterPanel.tsx +++ b/console/client/src/features/timeline/filters/TimelineFilterPanel.tsx @@ -49,10 +49,15 @@ export const TimelineFilterPanel = ({ const [selectedLogLevel, setSelectedLogLevel] = useState(1) useEffect(() => { + if (modules.modules.length === 0) { + return + } const newModules = modules.modules.map((module) => module.deploymentName) const addedModules = newModules.filter((name) => !previousModules.includes(name)) - setSelectedModules((prevSelected) => [...prevSelected, ...addedModules]) + if (addedModules.length > 0) { + setSelectedModules((prevSelected) => [...prevSelected, ...addedModules]) + } setPreviousModules(newModules) }, [modules]) diff --git a/console/client/src/features/verbs/VerbPage.tsx b/console/client/src/features/verbs/VerbPage.tsx index f77f7f732a..ca1205ae1c 100644 --- a/console/client/src/features/verbs/VerbPage.tsx +++ b/console/client/src/features/verbs/VerbPage.tsx @@ -40,8 +40,9 @@ export const VerbPage = () => { streamEvents({ abortControllerSignal: abortController.signal, filters: [callFilter(module.name, verb?.verb?.name), eventTypesFilter([EventType.CALL])], - onEventReceived: (event) => { - setCalls((prev) => [event.entry.value as CallEvent, ...prev]) + onEventsReceived: (events) => { + const callEvents = events.map((event) => event.entry.value as CallEvent) + setCalls((prev) => [...callEvents, ...prev]) }, }) } diff --git a/console/client/src/hooks/use-client.ts b/console/client/src/hooks/use-client.ts index 28a1dc9977..a194277696 100644 --- a/console/client/src/hooks/use-client.ts +++ b/console/client/src/hooks/use-client.ts @@ -1,6 +1,6 @@ -import { PromiseClient, createPromiseClient } from '@bufbuild/connect' -import { createConnectTransport } from '@bufbuild/connect-web' import { ServiceType } from '@bufbuild/protobuf' +import { PromiseClient, createPromiseClient } from '@connectrpc/connect' +import { createConnectTransport } from '@connectrpc/connect-web' import { useMemo } from 'react' const transport = createConnectTransport({ diff --git a/console/client/src/hooks/use-schema.ts b/console/client/src/hooks/use-schema.ts new file mode 100644 index 0000000000..36b58e5d22 --- /dev/null +++ b/console/client/src/hooks/use-schema.ts @@ -0,0 +1,66 @@ +import { Code, ConnectError } from '@connectrpc/connect' +import { useEffect, useState } from 'react' +import { useClient } from '../hooks/use-client' +import { useVisibility } from '../hooks/use-visibility.ts' +import { ControllerService } from '../protos/xyz/block/ftl/v1/ftl_connect.ts' +import { DeploymentChangeType, PullSchemaResponse } from '../protos/xyz/block/ftl/v1/ftl_pb' + +export const useSchema = () => { + const client = useClient(ControllerService) + const [schema, setSchema] = useState([]) + const isVisible = useVisibility() + + useEffect(() => { + const abortController = new AbortController() + + const fetchSchema = async () => { + try { + if (!isVisible) { + abortController.abort() + return + } + + const schemaMap = new Map() + for await (const response of client.pullSchema( + {}, + { + signal: abortController.signal, + }, + )) { + const moduleName = response.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) { + setSchema( + Array.from(schemaMap.values()).sort((a, b) => a.schema?.name?.localeCompare(b.schema?.name ?? '') ?? 0), + ) + } + } + } 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) + } + } + } + + fetchSchema() + return () => { + abortController.abort() + } + }, [client, isVisible]) + + return schema +} diff --git a/console/client/src/hooks/use-visibility.ts b/console/client/src/hooks/use-visibility.ts new file mode 100644 index 0000000000..a4998e97c9 --- /dev/null +++ b/console/client/src/hooks/use-visibility.ts @@ -0,0 +1,19 @@ +import { useEffect, useState } from 'react' + +export const useVisibility = () => { + const [isVisible, setIsVisible] = useState(document.visibilityState === 'visible') + + useEffect(() => { + const handleVisibilityChange = () => { + setIsVisible(document.visibilityState === 'visible') + } + + document.addEventListener('visibilitychange', handleVisibilityChange) + + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange) + } + }, []) + + return isVisible +} diff --git a/console/client/src/providers/AppProviders.tsx b/console/client/src/providers/AppProviders.tsx index 7ba10f2a7e..2773728a81 100644 --- a/console/client/src/providers/AppProviders.tsx +++ b/console/client/src/providers/AppProviders.tsx @@ -2,18 +2,15 @@ import { App } from '../App' import { DarkModeProvider } from './dark-mode-provider' import { ModulesProvider } from './modules-provider' import { NotificationsProvider } from './notifications-provider' -import { SchemaProvider } from './schema-provider' export const AppProviders = () => { return ( - - - - - - - + + + + + ) } diff --git a/console/client/src/providers/modules-provider.tsx b/console/client/src/providers/modules-provider.tsx index d776321a34..3130b122fd 100644 --- a/console/client/src/providers/modules-provider.tsx +++ b/console/client/src/providers/modules-provider.tsx @@ -1,20 +1,28 @@ -import { Code, ConnectError } from '@bufbuild/connect' -import { PropsWithChildren, createContext, useContext, useEffect, useState } from 'react' +import { Code, ConnectError } from '@connectrpc/connect' +import { PropsWithChildren, createContext, useEffect, useState } from 'react' import { useClient } from '../hooks/use-client' +import { useSchema } from '../hooks/use-schema' +import { useVisibility } from '../hooks/use-visibility' import { ConsoleService } from '../protos/xyz/block/ftl/v1/console/console_connect' import { GetModulesResponse } from '../protos/xyz/block/ftl/v1/console/console_pb' -import { schemaContext } from './schema-provider' export const modulesContext = createContext(new GetModulesResponse()) export const ModulesProvider = ({ children }: PropsWithChildren) => { - const schema = useContext(schemaContext) + const schema = useSchema() const client = useClient(ConsoleService) const [modules, setModules] = useState(new GetModulesResponse()) + const isVisible = useVisibility() useEffect(() => { const abortController = new AbortController() + const fetchModules = async () => { + if (!isVisible) { + abortController.abort() + return + } + try { const modules = await client.getModules({}, { signal: abortController.signal }) setModules(modules ?? []) @@ -35,7 +43,7 @@ export const ModulesProvider = ({ children }: PropsWithChildren) => { return () => { abortController.abort() } - }, [client, schema]) + }, [client, schema, isVisible]) return {children} } diff --git a/console/client/src/providers/schema-provider.tsx b/console/client/src/providers/schema-provider.tsx deleted file mode 100644 index 78b54ad43c..0000000000 --- a/console/client/src/providers/schema-provider.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { PropsWithChildren, createContext, useEffect, useState } from 'react' -import { useClient } from '../hooks/use-client' -import { ControllerService } from '../protos/xyz/block/ftl/v1/ftl_connect.ts' -import { DeploymentChangeType, PullSchemaResponse } from '../protos/xyz/block/ftl/v1/ftl_pb' - -export const schemaContext = createContext([]) - -export const SchemaProvider = ({ children }: PropsWithChildren) => { - const client = useClient(ControllerService) - const [schema, setSchema] = useState([]) - - useEffect(() => { - const abortController = new AbortController() - - const fetchSchema = async () => { - const schemaMap = new Map() - for await (const response of client.pullSchema({ - signal: abortController.signal, - })) { - const moduleName = response.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) { - setSchema( - Array.from(schemaMap.values()).sort((a, b) => a.schema?.name?.localeCompare(b.schema?.name ?? '') ?? 0), - ) - } - } - } - - fetchSchema() - return () => { - abortController.abort() - } - }, [client]) - - return {children} -} diff --git a/console/client/src/services/console.service.ts b/console/client/src/services/console.service.ts index 33e723b839..60aa77b9ba 100644 --- a/console/client/src/services/console.service.ts +++ b/console/client/src/services/console.service.ts @@ -1,5 +1,5 @@ -import { Code, ConnectError } from '@bufbuild/connect' import { 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 { @@ -161,19 +161,24 @@ export const getEvents = async ({ export const streamEvents = async ({ abortControllerSignal, filters, - onEventReceived, + onEventsReceived, }: { abortControllerSignal: AbortSignal filters: EventsQuery_Filter[] - onEventReceived: (event: Event) => void + onEventsReceived: (events: Event[]) => void }) => { try { + let events: Event[] = [] for await (const response of client.streamEvents( { updateInterval: { seconds: BigInt(1) }, query: { limit: 1000, filters } }, { signal: abortControllerSignal }, )) { if (response.event != null) { - onEventReceived(response.event) + events.push(response.event) + } + if (!response.more) { + onEventsReceived(events.reverse()) + events = [] } } } catch (error) {