From ae71a8a9155e4f34d9eb5f21f6e8c9dfe56f82eb Mon Sep 17 00:00:00 2001 From: Zheng Chen Date: Sat, 2 Dec 2023 05:08:00 +0800 Subject: [PATCH] feat(gui): Disconnect with host without refresh (#297) * fix(gui): some styles * feat(gui): Disconnect with host without refresh * feat(gui): add Connect Hosts in HomePage --- ui/web/src/modules/AccountInfo/model.ts | 6 +- ui/web/src/modules/Agent/AgentConfForm.tsx | 5 +- ui/web/src/modules/Agent/utils.ts | 5 +- ui/web/src/modules/Fund/RealtimeAsset.tsx | 2 + .../GeneralSpecificRelationList.tsx | 40 ++++++------ ui/web/src/modules/Market/Market.tsx | 13 +++- ui/web/src/modules/Market/SearchButton.tsx | 5 +- ui/web/src/modules/Order/ManualTradePanel.tsx | 5 +- ui/web/src/modules/Products/ProductList.tsx | 17 ++--- ui/web/src/modules/Products/model.ts | 4 +- .../PullSourceRelationList.tsx | 43 ++++++------- .../SubscriptionRelationList.tsx | 20 +++--- .../modules/Terminals/NetworkStatusWidget.tsx | 62 ++++++++++--------- ui/web/src/modules/Terminals/TerminalList.tsx | 19 +++--- .../modules/Terminals/TerminalListItem.tsx | 3 +- .../modules/Terminals/create-connection.ts | 17 +++-- ui/web/src/modules/Terminals/index.ts | 4 +- .../modules/TradeCopier/TradeConfigList.tsx | 20 +++--- .../TradeCopier/TradeCopyRelationList.tsx | 19 +++--- ui/web/src/modules/Workbench/HomePage.tsx | 13 +++- ui/web/src/modules/Workspace/Explorer.tsx | 2 + ui/web/src/modules/Workspace/airdrop.ts | 4 +- 22 files changed, 188 insertions(+), 140 deletions(-) diff --git a/ui/web/src/modules/AccountInfo/model.ts b/ui/web/src/modules/AccountInfo/model.ts index f0ec9605..63feccea 100644 --- a/ui/web/src/modules/AccountInfo/model.ts +++ b/ui/web/src/modules/AccountInfo/model.ts @@ -1,19 +1,19 @@ import { IAccountPerformance } from '@yuants/kernel'; import { IAccountInfo } from '@yuants/protocol'; -import { BehaviorSubject, Observable, defer, shareReplay, switchMap } from 'rxjs'; +import { BehaviorSubject, EMPTY, Observable, defer, of, shareReplay, switchMap } from 'rxjs'; import { terminal$ } from '../Terminals'; export const useAccountInfo = (() => { const hub: Record> = {}; return (account_id: string) => (hub[account_id] ??= defer(() => terminal$).pipe( - switchMap((terminal) => terminal.useAccountInfo(account_id)), + switchMap((terminal) => terminal?.useAccountInfo(account_id) ?? EMPTY), shareReplay(1), )); })(); export const accountIds$ = defer(() => terminal$).pipe( - switchMap((terminal) => terminal.accountIds$), + switchMap((terminal) => terminal?.accountIds$ ?? of([])), shareReplay(1), ); diff --git a/ui/web/src/modules/Agent/AgentConfForm.tsx b/ui/web/src/modules/Agent/AgentConfForm.tsx index 1bfe0cdd..26c10aa7 100644 --- a/ui/web/src/modules/Agent/AgentConfForm.tsx +++ b/ui/web/src/modules/Agent/AgentConfForm.tsx @@ -134,7 +134,9 @@ runAgentAction$.subscribe(async () => { console.error(validator.errors); throw msg; } - if (currentHostConfig$.value === null) { + const terminal = await firstValueFrom(terminal$); + + if (terminal === null) { const agentCode = await bundleCode(agentConf.entry!); const scene = await LocalAgentScene({ ...agentConf, bundled_code: agentCode }); const accountFrameUnit = new AccountFrameUnit( @@ -153,7 +155,6 @@ runAgentAction$.subscribe(async () => { ); accountFrameSeries$.next(accountFrameUnit.data); } else { - const terminal = await firstValueFrom(terminal$); const agentCode = await bundleCode(agentConf.entry!); const scene = await AgentScene(terminal, { ...agentConf, bundled_code: agentCode }); const accountFrameUnit = new AccountFrameUnit( diff --git a/ui/web/src/modules/Agent/utils.ts b/ui/web/src/modules/Agent/utils.ts index b04267fd..afff3b5b 100644 --- a/ui/web/src/modules/Agent/utils.ts +++ b/ui/web/src/modules/Agent/utils.ts @@ -322,8 +322,9 @@ export interface IBatchAgentResultItem { export const runBatchBackTestWorkItem = async (agentConf: IAgentConf): Promise => { if (!agentConf.bundled_code) throw new Error('No bundled_code'); - const scene = currentHostConfig$.value - ? await AgentScene(await firstValueFrom(terminal$), { + const terminal = await firstValueFrom(terminal$); + const scene = terminal + ? await AgentScene(terminal, { ...agentConf, disable_log: true, }) diff --git a/ui/web/src/modules/Fund/RealtimeAsset.tsx b/ui/web/src/modules/Fund/RealtimeAsset.tsx index 49ea9cef..1e032f82 100644 --- a/ui/web/src/modules/Fund/RealtimeAsset.tsx +++ b/ui/web/src/modules/Fund/RealtimeAsset.tsx @@ -324,6 +324,7 @@ registerPage('RealtimeAsset', () => { onClick={() => { terminal$ .pipe( + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => from(fundInfo.config.investors).pipe( @@ -343,6 +344,7 @@ registerPage('RealtimeAsset', () => { onClick={() => { terminal$ .pipe( + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => sendReportToInvestor(terminal, fundInfo, investor)), ) diff --git a/ui/web/src/modules/GeneralSpecificRelations/GeneralSpecificRelationList.tsx b/ui/web/src/modules/GeneralSpecificRelations/GeneralSpecificRelationList.tsx index 10e2b16f..44ccd3de 100644 --- a/ui/web/src/modules/GeneralSpecificRelations/GeneralSpecificRelationList.tsx +++ b/ui/web/src/modules/GeneralSpecificRelations/GeneralSpecificRelationList.tsx @@ -3,7 +3,7 @@ import { Button, Modal, Popconfirm, Space, Table, Toast } from '@douyinfe/semi-u import { IDataRecord } from '@yuants/protocol'; import { useObservable, useObservableState } from 'observable-hooks'; import { useState } from 'react'; -import { combineLatest, filter, first, mergeMap, tap, toArray } from 'rxjs'; +import { EMPTY, combineLatest, filter, first, mergeMap, tap, toArray } from 'rxjs'; import Form from '../Form'; import { registerPage } from '../Pages'; import { terminal$ } from '../Terminals'; @@ -63,28 +63,28 @@ registerPage('GeneralSpecificRelationList', () => { combineLatest([terminal$, input$]).pipe( // mergeMap(([terminal, [searchFormData]]) => - terminal - .queryDataRecords({ + ( + terminal?.queryDataRecords({ type: 'general_specific_relation', tags: {}, options: {}, - }) - .pipe( - // - filter( - (record) => - (searchFormData.general_product_id - ? record.origin.general_product_id === searchFormData.general_product_id - : true) && - (searchFormData.specific_datasource_id - ? record.origin.specific_datasource_id === searchFormData.specific_datasource_id - : true) && - (searchFormData.specific_product_id - ? record.origin.specific_product_id === searchFormData.specific_product_id - : true), - ), - toArray(), + }) ?? EMPTY + ).pipe( + // + filter( + (record) => + (searchFormData.general_product_id + ? record.origin.general_product_id === searchFormData.general_product_id + : true) && + (searchFormData.specific_datasource_id + ? record.origin.specific_datasource_id === searchFormData.specific_datasource_id + : true) && + (searchFormData.specific_product_id + ? record.origin.specific_product_id === searchFormData.specific_product_id + : true), ), + toArray(), + ), ), ), [searchFormData, refreshId], @@ -154,6 +154,7 @@ registerPage('GeneralSpecificRelationList', () => { terminal$ .pipe( // + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.removeDataRecords({ @@ -188,6 +189,7 @@ registerPage('GeneralSpecificRelationList', () => { const record = mapGeneralSpecificRelationToDataRecord(formData); terminal$ .pipe( + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.updateDataRecords([record])), tap({ diff --git a/ui/web/src/modules/Market/Market.tsx b/ui/web/src/modules/Market/Market.tsx index 88cc334a..2e049a66 100644 --- a/ui/web/src/modules/Market/Market.tsx +++ b/ui/web/src/modules/Market/Market.tsx @@ -13,7 +13,17 @@ import { import { t } from 'i18next'; import { useObservable, useObservableState } from 'observable-hooks'; import { useEffect, useMemo, useState } from 'react'; -import { BehaviorSubject, distinctUntilChanged, first, interval, map, mergeMap, tap, throwError } from 'rxjs'; +import { + BehaviorSubject, + distinctUntilChanged, + filter, + first, + interval, + map, + mergeMap, + tap, + throwError, +} from 'rxjs'; import { CandlestickSeries, Chart, ChartGroup } from '../Chart/components/Charts'; import { executeCommand, registerCommand } from '../CommandCenter'; import { showForm } from '../Form'; @@ -167,6 +177,7 @@ registerCommand('fetchOHLCV', async (params) => { terminal$ .pipe( + filter((x): x is Exclude => !!x), first(), tap(() => Toast.info(`开始拉取 ${datasource_id} / ${product_id} / ${period_in_sec} 历史数据...`)), mergeMap((terminal) => diff --git a/ui/web/src/modules/Market/SearchButton.tsx b/ui/web/src/modules/Market/SearchButton.tsx index 8cdfaa0f..0a1fbadc 100644 --- a/ui/web/src/modules/Market/SearchButton.tsx +++ b/ui/web/src/modules/Market/SearchButton.tsx @@ -2,7 +2,7 @@ import { Button, Descriptions, Modal, Space, Typography } from '@douyinfe/semi-u import { JSONSchema7 } from 'json-schema'; import { useObservable, useObservableState } from 'observable-hooks'; import React, { useMemo, useState } from 'react'; -import { defer, first, mergeMap, of, shareReplay, switchMap } from 'rxjs'; +import { defer, filter, first, mergeMap, of, shareReplay, switchMap } from 'rxjs'; import { executeCommand } from '../CommandCenter'; import { Form } from '../Form'; import { terminal$ } from '../Terminals'; @@ -20,7 +20,7 @@ const PERIOD_IN_SEC_TO_LABEL: Record = { }; export const datasourceIds$ = defer(() => terminal$).pipe( - switchMap((terminal) => terminal.datasourceIds$), + switchMap((terminal) => terminal?.datasourceIds$ ?? of([])), shareReplay(1), ); @@ -44,6 +44,7 @@ export const SearchButton = React.memo(() => { datasource_id ? terminal$.pipe( // + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.queryProducts({ datasource_id })), ) diff --git a/ui/web/src/modules/Order/ManualTradePanel.tsx b/ui/web/src/modules/Order/ManualTradePanel.tsx index acb7efb0..63542c6d 100644 --- a/ui/web/src/modules/Order/ManualTradePanel.tsx +++ b/ui/web/src/modules/Order/ManualTradePanel.tsx @@ -2,7 +2,7 @@ import { Button, Space, Toast } from '@douyinfe/semi-ui'; import { IOrder, OrderDirection, OrderType } from '@yuants/protocol'; import { useObservable, useObservableState } from 'observable-hooks'; import { useState } from 'react'; -import { first, mergeMap, of } from 'rxjs'; +import { filter, first, mergeMap, of } from 'rxjs'; import { accountIds$ } from '../AccountInfo/model'; import { Form } from '../Form'; import { registerPage } from '../Pages'; @@ -24,6 +24,7 @@ registerPage('ManualTradePanel', () => { mergeMap(([account_id]) => account_id ? terminal$.pipe( + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.queryProducts({ datasource_id: account_id })), ) @@ -93,6 +94,7 @@ registerPage('ManualTradePanel', () => { order && terminal$ .pipe( + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.submitOrder(order)), ) @@ -128,6 +130,7 @@ registerPage('ManualTradePanel', () => { cancelFormData && terminal$ .pipe( + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.cancelOrder(cancelFormData as IOrder)), ) diff --git a/ui/web/src/modules/Products/ProductList.tsx b/ui/web/src/modules/Products/ProductList.tsx index 179ae9e7..6e629760 100644 --- a/ui/web/src/modules/Products/ProductList.tsx +++ b/ui/web/src/modules/Products/ProductList.tsx @@ -5,7 +5,7 @@ import { IProduct } from '@yuants/protocol'; import { useObservable, useObservableState } from 'observable-hooks'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { combineLatest, first, mergeMap, tap, toArray } from 'rxjs'; +import { EMPTY, combineLatest, filter, first, mergeMap, tap, toArray } from 'rxjs'; import { executeCommand } from '../CommandCenter'; import Form, { showForm } from '../Form'; import { SearchButton } from '../Market/SearchButton'; @@ -25,8 +25,8 @@ registerPage('ProductList', () => { combineLatest([terminal$, input$]).pipe( // mergeMap(([terminal, [searchFormData]]) => - terminal - .queryDataRecords({ + ( + terminal?.queryDataRecords({ type: 'product', tags: { datasource_id: searchFormData.datasource_id || undefined, @@ -40,11 +40,11 @@ registerPage('ProductList', () => { ['tags.product_id', 1], ], }, - }) - .pipe( - // - toArray(), - ), + }) ?? EMPTY + ).pipe( + // + toArray(), + ), ), ), [searchFormData, refreshId], @@ -163,6 +163,7 @@ registerPage('ProductList', () => { onOk={() => { terminal$ .pipe( + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.updateProducts([formData])), tap({ diff --git a/ui/web/src/modules/Products/model.ts b/ui/web/src/modules/Products/model.ts index 1c4fbce4..8117a9fc 100644 --- a/ui/web/src/modules/Products/model.ts +++ b/ui/web/src/modules/Products/model.ts @@ -1,12 +1,12 @@ import { IProduct } from '@yuants/protocol'; -import { Observable, defer, shareReplay, switchMap } from 'rxjs'; +import { Observable, defer, of, shareReplay, switchMap } from 'rxjs'; import { terminal$ } from '../Terminals'; export const useProducts = (() => { const hub: Record> = {}; return (datasource_id: string) => (hub[datasource_id] ??= defer(() => terminal$).pipe( - switchMap((terminal) => terminal.useProducts(datasource_id)), + switchMap((terminal) => terminal?.useProducts(datasource_id) ?? of([])), shareReplay(1), )); })(); diff --git a/ui/web/src/modules/PullSourceRelations/PullSourceRelationList.tsx b/ui/web/src/modules/PullSourceRelations/PullSourceRelationList.tsx index d37a8dc2..c5348ea7 100644 --- a/ui/web/src/modules/PullSourceRelations/PullSourceRelationList.tsx +++ b/ui/web/src/modules/PullSourceRelations/PullSourceRelationList.tsx @@ -5,7 +5,7 @@ import { IDataRecord } from '@yuants/protocol'; import { JSONSchema7 } from 'json-schema'; import { useObservable, useObservableState } from 'observable-hooks'; import { useState } from 'react'; -import { combineLatest, filter, first, mergeMap, tap, toArray } from 'rxjs'; +import { EMPTY, combineLatest, filter, first, mergeMap, tap, toArray } from 'rxjs'; import { executeCommand } from '../CommandCenter'; import { showForm } from '../Form'; import { registerPage } from '../Pages'; @@ -84,8 +84,8 @@ registerPage('PullSourceRelationList', () => { combineLatest([terminal$, input$]).pipe( // mergeMap(([terminal, [searchFormData]]) => - terminal - .queryDataRecords({ + ( + terminal?.queryDataRecords({ type: 'pull_source_relation', tags: {}, options: { @@ -95,25 +95,23 @@ registerPage('PullSourceRelationList', () => { ['origin.period_in_sec', 1], ], }, - }) - .pipe( - filter( - (record) => - !searchFormData.datasource_id || - record.origin.datasource_id === searchFormData.datasource_id, - ), - filter( - (record) => - !searchFormData.product_id || record.origin.product_id === searchFormData.product_id, - ), - filter( - (record) => - !searchFormData.period_in_sec || - record.origin.period_in_sec === searchFormData.period_in_sec, - ), - // - toArray(), + }) ?? EMPTY + ).pipe( + filter( + (record) => + !searchFormData.datasource_id || record.origin.datasource_id === searchFormData.datasource_id, ), + filter( + (record) => + !searchFormData.product_id || record.origin.product_id === searchFormData.product_id, + ), + filter( + (record) => + !searchFormData.period_in_sec || record.origin.period_in_sec === searchFormData.period_in_sec, + ), + // + toArray(), + ), ), ), [searchFormData, refreshId], @@ -124,6 +122,7 @@ registerPage('PullSourceRelationList', () => { const record = mapPullSourceRelationToDataRecord(formData); terminal$ .pipe( + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.updateDataRecords([record])), tap({ @@ -186,6 +185,7 @@ registerPage('PullSourceRelationList', () => { const next = mapPullSourceRelationToDataRecord({ ...record.origin, disabled: v }); terminal$ .pipe( + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.updateDataRecords([next])), tap({ @@ -231,6 +231,7 @@ registerPage('PullSourceRelationList', () => { terminal$ .pipe( // + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.removeDataRecords({ diff --git a/ui/web/src/modules/SubscriptionRelation/SubscriptionRelationList.tsx b/ui/web/src/modules/SubscriptionRelation/SubscriptionRelationList.tsx index 16d8e805..5e8aa8e9 100644 --- a/ui/web/src/modules/SubscriptionRelation/SubscriptionRelationList.tsx +++ b/ui/web/src/modules/SubscriptionRelation/SubscriptionRelationList.tsx @@ -3,10 +3,10 @@ import { Button, Modal, Popconfirm, Space, Table, Toast } from '@douyinfe/semi-u import { IDataRecord, ISubscriptionRelation } from '@yuants/protocol'; import { useObservable, useObservableState } from 'observable-hooks'; import { useState } from 'react'; -import { combineLatest, first, mergeMap, tap, toArray } from 'rxjs'; -import { terminal$ } from '../Terminals'; +import { EMPTY, combineLatest, filter, first, mergeMap, tap, toArray } from 'rxjs'; import Form from '../Form'; import { registerPage } from '../Pages'; +import { terminal$ } from '../Terminals'; const TYPE = 'subscription_relation'; @@ -57,8 +57,8 @@ registerPage('SubscriptionRelationList', () => { combineLatest([terminal$, input$]).pipe( // mergeMap(([terminal, [searchFormData]]) => - terminal - .queryDataRecords({ + ( + terminal?.queryDataRecords({ type: TYPE, tags: { ...(searchFormData.channel_id ? { channel_id: searchFormData.channel_id } : {}), @@ -69,11 +69,11 @@ registerPage('SubscriptionRelationList', () => { ? { consumer_terminal_id: searchFormData.consumer_terminal_id } : {}), }, - }) - .pipe( - // - toArray(), - ), + }) ?? EMPTY + ).pipe( + // + toArray(), + ), ), ), [searchFormData, refreshId], @@ -143,6 +143,7 @@ registerPage('SubscriptionRelationList', () => { terminal$ .pipe( // + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.removeDataRecords({ @@ -176,6 +177,7 @@ registerPage('SubscriptionRelationList', () => { const record = mapSubscriptionRelationToDataRecord(formData); terminal$ .pipe( + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.updateDataRecords([record])), tap({ diff --git a/ui/web/src/modules/Terminals/NetworkStatusWidget.tsx b/ui/web/src/modules/Terminals/NetworkStatusWidget.tsx index 782413dc..a59cb829 100644 --- a/ui/web/src/modules/Terminals/NetworkStatusWidget.tsx +++ b/ui/web/src/modules/Terminals/NetworkStatusWidget.tsx @@ -3,7 +3,7 @@ import { Button, Card, Descriptions, Popover, Space, Typography } from '@douyinf import { useObservable, useObservableState } from 'observable-hooks'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { bufferTime, combineLatest, map, switchMap } from 'rxjs'; +import { bufferTime, combineLatest, map, of, switchMap } from 'rxjs'; import { executeCommand } from '../CommandCenter'; import { terminal$ } from '../Terminals'; import { currentHostConfig$ } from '../Workbench/model'; @@ -25,16 +25,18 @@ export const NetworkStatusWidget = React.memo(() => { const network$ = useObservable(() => terminal$.pipe( switchMap((terminal) => - combineLatest([ - terminal._conn.output$.pipe( - bufferTime(2000), - map((buffer) => ((JSON.stringify(buffer).length / 2e3) * 8).toFixed(1)), - ), - terminal._conn.input$.pipe( - bufferTime(2000), - map((buffer) => ((JSON.stringify(buffer).length / 2e3) * 8).toFixed(1)), - ), - ]), + terminal + ? combineLatest([ + terminal._conn.output$.pipe( + bufferTime(2000), + map((buffer) => ((JSON.stringify(buffer).length / 2e3) * 8).toFixed(1)), + ), + terminal._conn.input$.pipe( + bufferTime(2000), + map((buffer) => ((JSON.stringify(buffer).length / 2e3) * 8).toFixed(1)), + ), + ]) + : of(['0.0', '0.0']), ), ), ); @@ -43,7 +45,6 @@ export const NetworkStatusWidget = React.memo(() => { return ( { }, ]} > - - + + + + } > diff --git a/ui/web/src/modules/Terminals/TerminalList.tsx b/ui/web/src/modules/Terminals/TerminalList.tsx index 42ae70b2..7f2bee24 100644 --- a/ui/web/src/modules/Terminals/TerminalList.tsx +++ b/ui/web/src/modules/Terminals/TerminalList.tsx @@ -1,21 +1,22 @@ import { List } from '@douyinfe/semi-ui'; import { ITerminalInfo } from '@yuants/protocol'; import { useObservableState } from 'observable-hooks'; -import { defer, repeat, retry, shareReplay, switchMap, toArray } from 'rxjs'; +import { EMPTY, defer, repeat, retry, shareReplay, switchMap, toArray } from 'rxjs'; import { registerPage } from '../Pages'; import { TerminalListItem } from './TerminalListItem'; import { terminal$ } from './create-connection'; export const terminalList$ = terminal$.pipe( switchMap((terminal) => - defer(() => - terminal.queryDataRecords( - { - type: 'terminal_info', - options: { sort: [['tags.terminal_id', 1]] }, - }, - 'MongoDB', - ), + defer( + () => + terminal?.queryDataRecords( + { + type: 'terminal_info', + options: { sort: [['tags.terminal_id', 1]] }, + }, + 'MongoDB', + ) ?? EMPTY, ).pipe( // toArray(), diff --git a/ui/web/src/modules/Terminals/TerminalListItem.tsx b/ui/web/src/modules/Terminals/TerminalListItem.tsx index ede7d84f..7075bb2c 100644 --- a/ui/web/src/modules/Terminals/TerminalListItem.tsx +++ b/ui/web/src/modules/Terminals/TerminalListItem.tsx @@ -4,7 +4,7 @@ import { formatTime } from '@yuants/data-model'; import { IDataRecord, ITerminalInfo } from '@yuants/protocol'; import { formatDuration, intervalToDuration } from 'date-fns'; import React from 'react'; -import { EMPTY, catchError, first, mergeMap, tap } from 'rxjs'; +import { EMPTY, catchError, filter, first, mergeMap, tap } from 'rxjs'; import { terminal$ } from '../Terminals'; export const TerminalListItem = React.memo((props: { terminalInfo: IDataRecord }) => { @@ -68,6 +68,7 @@ export const TerminalListItem = React.memo((props: { terminalInfo: IDataRecord => !!x), first(), mergeMap((terminal) => terminal.request('Terminate', terminal_id, {}).pipe( diff --git a/ui/web/src/modules/Terminals/create-connection.ts b/ui/web/src/modules/Terminals/create-connection.ts index 1821a825..21ace1b0 100644 --- a/ui/web/src/modules/Terminals/create-connection.ts +++ b/ui/web/src/modules/Terminals/create-connection.ts @@ -2,13 +2,14 @@ import { Terminal } from '@yuants/protocol'; import { Observable, filter, shareReplay, switchMap } from 'rxjs'; import { currentHostConfig$ } from '../Workbench/model'; -export const terminal$ = currentHostConfig$.pipe( - filter( - (config): config is Exclude => - !!(config && config.host_url && config.terminal_id), - ), +export const terminal$: Observable = currentHostConfig$.pipe( + filter((config): config is Exclude => config !== undefined), switchMap((config) => { - return new Observable((subscriber) => { + return new Observable((subscriber) => { + if (!config) { + subscriber.next(null); + return; + } const terminal = new Terminal(config.host_url, { terminal_id: config.terminal_id, name: 'Workbench GUI', @@ -25,10 +26,8 @@ export const terminal$ = currentHostConfig$.pipe( ); terminal$.forEach((terminal) => { - const connection = terminal._conn; - // for DEBUG - connection.connection$.forEach((conn) => Object.assign(globalThis, { _conn: conn })); + terminal?._conn.connection$.forEach((conn) => Object.assign(globalThis, { _conn: conn })); Object.assign(globalThis, { terminal }); }); diff --git a/ui/web/src/modules/Terminals/index.ts b/ui/web/src/modules/Terminals/index.ts index 7d031d76..b2e15af7 100644 --- a/ui/web/src/modules/Terminals/index.ts +++ b/ui/web/src/modules/Terminals/index.ts @@ -1,5 +1,5 @@ import { IPeriod, ITick } from '@yuants/protocol'; -import { Observable, defer, shareReplay, switchMap } from 'rxjs'; +import { Observable, defer, filter, shareReplay, switchMap } from 'rxjs'; import './TerminalList'; import { terminal$ } from './create-connection'; export { terminal$ } from './create-connection'; @@ -8,6 +8,7 @@ export const useTick = (() => { const hub: Record> = {}; return (datasource_id: string, product_id: string) => (hub[[datasource_id, product_id].join('\n')] ??= defer(() => terminal$).pipe( + filter((x): x is Exclude => !!x), switchMap((terminal) => terminal.useTick(datasource_id, product_id)), shareReplay(1), )); @@ -17,6 +18,7 @@ export const usePeriod = (() => { const hub: Record> = {}; return (datasource_id: string, product_id: string, period_in_sec: number) => (hub[[datasource_id, product_id, period_in_sec].join('\n')] ??= defer(() => terminal$).pipe( + filter((x): x is Exclude => !!x), switchMap((terminal) => terminal.usePeriod(datasource_id, product_id, period_in_sec)), shareReplay(1), )); diff --git a/ui/web/src/modules/TradeCopier/TradeConfigList.tsx b/ui/web/src/modules/TradeCopier/TradeConfigList.tsx index e24553af..93322f27 100644 --- a/ui/web/src/modules/TradeCopier/TradeConfigList.tsx +++ b/ui/web/src/modules/TradeCopier/TradeConfigList.tsx @@ -4,10 +4,10 @@ import { UUID } from '@yuants/data-model'; import { IDataRecord } from '@yuants/protocol'; import { useObservable, useObservableState } from 'observable-hooks'; import { useState } from 'react'; -import { combineLatest, first, mergeMap, tap, toArray } from 'rxjs'; -import { terminal$ } from '../Terminals'; +import { EMPTY, combineLatest, filter, first, mergeMap, tap, toArray } from 'rxjs'; import Form from '../Form'; import { registerPage } from '../Pages'; +import { terminal$ } from '../Terminals'; interface ITradeCopierTradeConfig { id?: string; @@ -63,14 +63,14 @@ registerPage('TradeConfigList', () => { combineLatest([terminal$, input$]).pipe( // mergeMap(([terminal, [searchFormData]]) => - terminal - .queryDataRecords({ + ( + terminal?.queryDataRecords({ type: TYPE, - }) - .pipe( - // - toArray(), - ), + }) ?? EMPTY + ).pipe( + // + toArray(), + ), ), ), [searchFormData, refreshId], @@ -139,6 +139,7 @@ registerPage('TradeConfigList', () => { terminal$ .pipe( // + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.removeDataRecords({ @@ -172,6 +173,7 @@ registerPage('TradeConfigList', () => { const record = mapTradeCopierTradeConfigToDataRecord(formData); terminal$ .pipe( + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.updateDataRecords([record])), tap({ diff --git a/ui/web/src/modules/TradeCopier/TradeCopyRelationList.tsx b/ui/web/src/modules/TradeCopier/TradeCopyRelationList.tsx index 79efd09b..47c85eef 100644 --- a/ui/web/src/modules/TradeCopier/TradeCopyRelationList.tsx +++ b/ui/web/src/modules/TradeCopier/TradeCopyRelationList.tsx @@ -5,7 +5,7 @@ import { IDataRecord } from '@yuants/protocol'; import { useObservable, useObservableState } from 'observable-hooks'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { combineLatest, first, mergeMap, tap, toArray } from 'rxjs'; +import { EMPTY, combineLatest, filter, first, mergeMap, tap, toArray } from 'rxjs'; import { InlineAccountId } from '../AccountInfo'; import { registerCommand } from '../CommandCenter'; import Form from '../Form'; @@ -91,8 +91,8 @@ registerPage('TradeCopyRelationList', () => { combineLatest([terminal$, input$]).pipe( // mergeMap(([terminal, [searchFormData]]) => - terminal - .queryDataRecords({ + ( + terminal?.queryDataRecords({ type: TYPE, options: { sort: [ @@ -100,11 +100,11 @@ registerPage('TradeCopyRelationList', () => { ['origin.source_product_id', 1], ], }, - }) - .pipe( - // - toArray(), - ), + }) ?? EMPTY + ).pipe( + // + toArray(), + ), ), ), [searchFormData, refreshId], @@ -174,6 +174,7 @@ registerPage('TradeCopyRelationList', () => { const next = mapTradeCopyRelationToDataRecord({ ...record.origin, disabled: v }); terminal$ .pipe( + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.updateDataRecords([next])), tap({ @@ -209,6 +210,7 @@ registerPage('TradeCopyRelationList', () => { terminal$ .pipe( // + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.removeDataRecords({ @@ -242,6 +244,7 @@ registerPage('TradeCopyRelationList', () => { const record = mapTradeCopyRelationToDataRecord(formData); terminal$ .pipe( + filter((x): x is Exclude => !!x), first(), mergeMap((terminal) => terminal.updateDataRecords([record])), tap({ diff --git a/ui/web/src/modules/Workbench/HomePage.tsx b/ui/web/src/modules/Workbench/HomePage.tsx index 5edea71c..5538a8c3 100644 --- a/ui/web/src/modules/Workbench/HomePage.tsx +++ b/ui/web/src/modules/Workbench/HomePage.tsx @@ -1,4 +1,4 @@ -import { IconFolderOpen } from '@douyinfe/semi-icons'; +import { IconFolderOpen, IconLink } from '@douyinfe/semi-icons'; import { Space, Typography } from '@douyinfe/semi-ui'; import { Book, Github, SmartOptimization } from '@icon-park/react'; import React from 'react'; @@ -39,6 +39,17 @@ export const HomePage = React.memo(() => { > Open Workspace... + } + strong + link={{ + onClick: () => { + executeCommand('HostList'); + }, + }} + > + Connect Hosts... + } strong diff --git a/ui/web/src/modules/Workspace/Explorer.tsx b/ui/web/src/modules/Workspace/Explorer.tsx index 738ed3a9..7ef91e3f 100644 --- a/ui/web/src/modules/Workspace/Explorer.tsx +++ b/ui/web/src/modules/Workspace/Explorer.tsx @@ -27,6 +27,7 @@ import { executeCommand, registerCommand } from '../CommandCenter'; import { installExtensionFromTgz } from '../Extensions/utils'; import { FsBackend$, fs, workspaceRoot$ } from '../FileSystem/api'; import { showForm } from '../Form'; +import i18n from '../Locale/i18n'; import { registerPage } from '../Pages'; import { terminal$ } from '../Terminals'; import { currentHostConfig$ } from '../Workbench/model'; @@ -351,6 +352,7 @@ registerCommand('CreateDirectory', async ({ baseDir = '/' }) => { }); registerCommand('workspace.open', async () => { + await i18n.loadNamespaces('Explorer'); Modal.confirm({ title: t('Explorer:request_fs_permission'), content: , diff --git a/ui/web/src/modules/Workspace/airdrop.ts b/ui/web/src/modules/Workspace/airdrop.ts index 5c1267f5..f0dcaa15 100644 --- a/ui/web/src/modules/Workspace/airdrop.ts +++ b/ui/web/src/modules/Workspace/airdrop.ts @@ -2,8 +2,8 @@ import { Modal, Toast } from '@douyinfe/semi-ui'; import { Terminal } from '@yuants/protocol'; import { dirname } from 'path-browserify'; import { bufferTime, defer, lastValueFrom, mergeMap } from 'rxjs'; -import { terminal$ } from '../Terminals'; import { fs } from '../FileSystem/api'; +import { terminal$ } from '../Terminals'; export const sendFileByAirdrop = async (terminal: Terminal, target_terminal_id: string, filename: string) => { // @@ -30,7 +30,7 @@ declare module '@yuants/protocol/lib/services' { } terminal$.subscribe((terminal) => { - terminal.setupService('AirDrop', (msg) => + terminal?.setupService('AirDrop', (msg) => defer(async () => { const { filename, content } = msg.req; const ok = await new Promise((resolve, reject) => {