feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { IconCozFocus } from '@coze-arch/coze-design/icons';
|
||||
import { IconButton } from '@coze-arch/coze-design';
|
||||
import { type Span } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { getStrFromSpan } from '../../utils';
|
||||
|
||||
export const FocusButton: React.FC<{
|
||||
span: Span;
|
||||
onClick: (span: Span) => void;
|
||||
}> = ({ span, onClick }) => {
|
||||
const nodeId = useMemo(
|
||||
() => getStrFromSpan(span, 'workflow_node_id'),
|
||||
[span],
|
||||
);
|
||||
|
||||
if (!nodeId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={<IconCozFocus />}
|
||||
size="mini"
|
||||
onClick={() => onClick(span)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { FocusButton } from './focus-button';
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { clsx } from 'clsx';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
IconCozCheckMarkCircleFillPalette,
|
||||
IconCozCrossCircleFillPalette,
|
||||
} from '@coze-arch/coze-design/icons';
|
||||
import { Tag } from '@coze-arch/coze-design';
|
||||
|
||||
interface StatusTagProps {
|
||||
status?: number;
|
||||
className?: string;
|
||||
type?: 'normal' | 'icon';
|
||||
}
|
||||
|
||||
export const StatusIcon: React.FC<{ status?: number; className?: string }> = ({
|
||||
status,
|
||||
className,
|
||||
}) =>
|
||||
status === 0 ? (
|
||||
<IconCozCheckMarkCircleFillPalette
|
||||
className={clsx(className, 'coz-fg-hglt-green')}
|
||||
/>
|
||||
) : (
|
||||
<IconCozCrossCircleFillPalette
|
||||
className={clsx(className, 'coz-fg-hglt-red')}
|
||||
/>
|
||||
);
|
||||
|
||||
export const StatusTag: React.FC<StatusTagProps> = ({
|
||||
status,
|
||||
className,
|
||||
type = 'normal',
|
||||
}) => {
|
||||
const children = useMemo(() => {
|
||||
if (type === 'icon') {
|
||||
return null;
|
||||
}
|
||||
return status === 0
|
||||
? I18n.t('debug_asyn_task_task_status_success')
|
||||
: I18n.t('debug_asyn_task_task_status_failed');
|
||||
}, [status, type]);
|
||||
|
||||
return (
|
||||
<Tag
|
||||
prefixIcon={
|
||||
status === 0 ? (
|
||||
<IconCozCheckMarkCircleFillPalette />
|
||||
) : (
|
||||
<IconCozCrossCircleFillPalette />
|
||||
)
|
||||
}
|
||||
color={status === 0 ? 'green' : 'red'}
|
||||
className={className}
|
||||
size="mini"
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { TraceDetailPanel } from './trace-detail-panel';
|
||||
@@ -0,0 +1,12 @@
|
||||
.pay-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 2px;
|
||||
}
|
||||
|
||||
.pay-blocks {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 2px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Divider, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import styles from './pay-block.module.less';
|
||||
|
||||
interface PayBlockProps {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const PayBlock: React.FC<PayBlockProps> = ({ label, value }) => (
|
||||
<div className={styles['pay-block']}>
|
||||
<Typography.Text type="secondary" size="small">
|
||||
{label}:
|
||||
</Typography.Text>
|
||||
<Typography.Text strong size="small">
|
||||
{value}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
|
||||
interface PayBlocksProps {
|
||||
options: PayBlockProps[];
|
||||
}
|
||||
|
||||
export const PayBlocks: React.FC<PayBlocksProps> = ({ options }) => (
|
||||
<div className={styles['pay-blocks']}>
|
||||
{options.flatMap((item, idx) =>
|
||||
idx < options.length - 1
|
||||
? [
|
||||
<PayBlock key={item.label} {...item} />,
|
||||
<Divider layout="vertical" margin={4} style={{ height: '10px' }} />,
|
||||
]
|
||||
: [<PayBlock key={item.label} {...item} />],
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
.trace-detail-panel {
|
||||
width: 360px;
|
||||
min-width:360px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.trace-detail {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.detail-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
margin-bottom: 4px;
|
||||
:global .coz-icon-button {
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
.log-id {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
:global .coz-icon-button {
|
||||
line-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.json-viewer {
|
||||
margin-bottom: 16px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { BottomPanel } from '@coze-workflow/test-run-shared';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type TraceFrontendSpan } from '@coze-arch/bot-api/workflow_api';
|
||||
import { IconCozCopy } from '@coze-arch/coze-design/icons';
|
||||
import { Divider, IconButton, Toast, Typography } from '@coze-arch/coze-design';
|
||||
|
||||
import { StatusTag } from '../status-tag';
|
||||
import { FocusButton } from '../focus-button';
|
||||
import {
|
||||
formatDuration,
|
||||
getTokensFromSpan,
|
||||
getGotoNodeParams,
|
||||
} from '../../utils';
|
||||
import { type GotoParams } from '../../types';
|
||||
import {
|
||||
MessagePanel,
|
||||
ObservationModules,
|
||||
type MessagePanelProps,
|
||||
} from '../../observation-components';
|
||||
import { PayBlocks } from './pay-block';
|
||||
|
||||
import styles from './trace-detail-panel.module.less';
|
||||
|
||||
const ResultViewer: React.FC<
|
||||
Omit<MessagePanelProps, 'i18nMapping'>
|
||||
> = props => (
|
||||
<MessagePanel
|
||||
{...props}
|
||||
className={styles['json-viewer']}
|
||||
i18nMapping={
|
||||
{
|
||||
[ObservationModules.INPUT]: {
|
||||
title: I18n.t('workflow_detail_node_input'),
|
||||
},
|
||||
[ObservationModules.OUTPUT]: {
|
||||
title: I18n.t('workflow_detail_node_output'),
|
||||
},
|
||||
} as unknown as MessagePanelProps['i18nMapping']
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
interface TraceDetailPanelProps {
|
||||
span: TraceFrontendSpan;
|
||||
onClose: () => void;
|
||||
onGotoNode: (params: GotoParams) => void;
|
||||
}
|
||||
|
||||
export const TraceDetailPanel: React.FC<TraceDetailPanelProps> = ({
|
||||
span,
|
||||
onClose,
|
||||
onGotoNode,
|
||||
}) => {
|
||||
const pays = useMemo(() => {
|
||||
const temp = [
|
||||
{
|
||||
label: I18n.t('analytic_query_detail_key_latency'),
|
||||
value: span.duration
|
||||
? formatDuration(span.duration as unknown as number)
|
||||
: '0ms',
|
||||
},
|
||||
];
|
||||
const tokens = getTokensFromSpan(span);
|
||||
if (!isUndefined(tokens)) {
|
||||
temp.push({
|
||||
label: I18n.t('analytic_query_table_title_tokens'),
|
||||
value: `${tokens}`,
|
||||
});
|
||||
}
|
||||
return temp;
|
||||
}, [span]);
|
||||
|
||||
const handleCopy = () => {
|
||||
try {
|
||||
copy(span.log_id || '');
|
||||
Toast.success({ content: I18n.t('copy_success'), showClose: false });
|
||||
} catch {
|
||||
Toast.error(I18n.t('copy_failed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
onGotoNode(getGotoNodeParams(span));
|
||||
};
|
||||
|
||||
return (
|
||||
<BottomPanel
|
||||
header={I18n.t('workflow_running_results')}
|
||||
onClose={onClose}
|
||||
className={styles['trace-detail-panel']}
|
||||
>
|
||||
<div className={styles['trace-detail']}>
|
||||
<div className={styles['detail-title']}>
|
||||
<Typography.Text strong>{span.alias_name}</Typography.Text>
|
||||
<StatusTag status={span.status_code} />
|
||||
<FocusButton span={span} onClick={handleScroll} />
|
||||
</div>
|
||||
<PayBlocks options={pays} />
|
||||
{span.log_id ? (
|
||||
<div className={styles['log-id']}>
|
||||
<Typography.Text type="secondary" size="small">
|
||||
LogId: {span.log_id}
|
||||
</Typography.Text>
|
||||
<IconButton
|
||||
icon={<IconCozCopy />}
|
||||
size="mini"
|
||||
onClick={handleCopy}
|
||||
color="secondary"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<Divider margin={16} />
|
||||
{span.input?.content ? (
|
||||
<ResultViewer
|
||||
content={span.input?.content}
|
||||
category={ObservationModules.INPUT}
|
||||
jsonViewerProps={{
|
||||
displayDataTypes: false,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{span.output?.content ? (
|
||||
<ResultViewer
|
||||
content={span.output?.content}
|
||||
category={ObservationModules.OUTPUT}
|
||||
jsonViewerProps={{
|
||||
displayDataTypes: false,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</BottomPanel>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
.trace-graph {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
:global .coz-icon-button-mini {
|
||||
line-height: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.graph-part {
|
||||
width: 50%;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.part-tree {
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.part-chart {
|
||||
border-left: 1px solid var(--coz-stroke-plus);
|
||||
}
|
||||
|
||||
.trace-charts {
|
||||
height: 100%;
|
||||
}
|
||||
.chart-header {
|
||||
height: 48px;
|
||||
padding: 0 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid var(--coz-stroke-plus);
|
||||
|
||||
.mode-select {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
height: calc(100% - 48px);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { clsx } from 'clsx';
|
||||
import { gotoDebugFlow } from '@coze-workflow/test-run-shared';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import {
|
||||
type Span,
|
||||
type TraceFrontendSpan,
|
||||
} from '@coze-arch/bot-api/workflow_api';
|
||||
import { IconCozExit } from '@coze-arch/coze-design/icons';
|
||||
import { Typography, Select, IconButton } from '@coze-arch/coze-design';
|
||||
|
||||
import { FocusButton } from '../focus-button';
|
||||
import { getGotoNodeParams } from '../../utils';
|
||||
import { type GotoParams } from '../../types';
|
||||
import {
|
||||
TraceTree,
|
||||
TraceFlameThread,
|
||||
spans2SpanNodes,
|
||||
} from '../../observation-components';
|
||||
import { useTraceListStore } from '../../contexts';
|
||||
import { TraceChartsMode } from '../../constants';
|
||||
import { useTrace } from './use-trace';
|
||||
import { EmptyTemplate, LoadingTemplate } from './template';
|
||||
import { TraceTable } from './table';
|
||||
|
||||
import css from './graph.module.less';
|
||||
|
||||
interface TraceGraphProps {
|
||||
onOpenDetail: (span: TraceFrontendSpan) => void;
|
||||
onGotoNode: (params: GotoParams) => void;
|
||||
}
|
||||
|
||||
const MODE_OPTIONS = [
|
||||
{
|
||||
label: I18n.t('analytic_query_detail_left_panel_flamethread'),
|
||||
value: TraceChartsMode.FlameThread,
|
||||
},
|
||||
{
|
||||
label: I18n.t('Starling_filebox_api_list'),
|
||||
value: TraceChartsMode.Table,
|
||||
},
|
||||
];
|
||||
|
||||
export const TraceGraph: React.FC<TraceGraphProps> = ({
|
||||
onOpenDetail,
|
||||
onGotoNode,
|
||||
}) => {
|
||||
const [mode, setMode] = useState(TraceChartsMode.FlameThread);
|
||||
const { ready, spaceId, isInOp } = useTraceListStore(store => ({
|
||||
ready: store.ready,
|
||||
spaceId: store.spaceId,
|
||||
isInOp: store.isInOp,
|
||||
}));
|
||||
const { spans, loading } = useTrace();
|
||||
|
||||
const tree = useMemo(() => spans2SpanNodes(spans || ([] as any)), [spans]);
|
||||
|
||||
const handleFocusNode = (span: Span) => {
|
||||
onGotoNode(getGotoNodeParams(span));
|
||||
};
|
||||
|
||||
const jumpToDebugFlow = (span: Span) => {
|
||||
const params = getGotoNodeParams(span);
|
||||
gotoDebugFlow(
|
||||
{
|
||||
...params,
|
||||
spaceId,
|
||||
},
|
||||
isInOp,
|
||||
);
|
||||
};
|
||||
|
||||
if (!ready || loading) {
|
||||
return <LoadingTemplate />;
|
||||
}
|
||||
|
||||
if (!spans) {
|
||||
return <EmptyTemplate />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css['trace-graph']}>
|
||||
<div className={clsx(css['graph-part'], css['part-tree'])}>
|
||||
<TraceTree
|
||||
spans={tree.roots}
|
||||
renderGraphNodeConfig={{
|
||||
traceTreeCustomRenderer: {
|
||||
renderExtra: span =>
|
||||
span.parent_id && span.parent_id !== '0' ? (
|
||||
<FocusButton span={span} onClick={handleFocusNode} />
|
||||
) : (
|
||||
<IconButton
|
||||
size="mini"
|
||||
icon={<IconCozExit />}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
jumpToDebugFlow(span);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
onSelect={v => {
|
||||
const span = (v.node.extra as any)?.spanNode;
|
||||
if (span) {
|
||||
onOpenDetail(span);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={clsx(css['graph-part'], css['part-chart'])}>
|
||||
<div className={css['trace-charts']}>
|
||||
<div className={css['chart-header']}>
|
||||
<Typography.Text strong fontSize="16px">
|
||||
{I18n.t('store_bot_detail_title_mobile')}
|
||||
</Typography.Text>
|
||||
<Select
|
||||
size="small"
|
||||
className={css['mode-select']}
|
||||
optionList={MODE_OPTIONS}
|
||||
value={mode}
|
||||
onChange={(v: any) => setMode(v)}
|
||||
/>
|
||||
</div>
|
||||
<div className={css['chart-content']}>
|
||||
{mode === TraceChartsMode.Table && <TraceTable spans={spans} />}
|
||||
{mode === TraceChartsMode.FlameThread && (
|
||||
<TraceFlameThread spans={spans} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { TraceGraph } from './graph';
|
||||
@@ -0,0 +1,20 @@
|
||||
.trace-table {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
:global {
|
||||
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
|
||||
height: 32px !important;
|
||||
padding-top: 4px !important;
|
||||
padding-bottom: 4px !important;
|
||||
}
|
||||
.semi-table-row-head {
|
||||
font-size: 12px !important;
|
||||
height: 32px !important;
|
||||
padding-top: 4px !important;
|
||||
padding-bottom: 4px !important;
|
||||
}
|
||||
.coz-table-wrapper .coz-table-list .semi-table-fixed-header table {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { isUndefined } from 'lodash-es';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { Table } from '@coze-arch/coze-design';
|
||||
import { type TraceFrontendSpan } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { formatDuration, getTokensFromSpan } from '../../utils';
|
||||
|
||||
import css from './table.module.less';
|
||||
|
||||
interface TraceTableProps {
|
||||
spans: TraceFrontendSpan[];
|
||||
}
|
||||
|
||||
export const TraceTable: React.FC<TraceTableProps> = ({ spans }) => {
|
||||
const columns = [
|
||||
{
|
||||
title: I18n.t('platfrom_trigger_creat_name'),
|
||||
dataIndex: 'name',
|
||||
},
|
||||
{
|
||||
title: I18n.t('debug_asyn_task_task_status'),
|
||||
dataIndex: 'status_code',
|
||||
render: data =>
|
||||
data === 0
|
||||
? I18n.t('debug_asyn_task_task_status_success')
|
||||
: I18n.t('debug_asyn_task_task_status_failed'),
|
||||
width: 78,
|
||||
},
|
||||
{
|
||||
title: I18n.t('analytic_query_table_title_tokens'),
|
||||
render: (_, row) => {
|
||||
const v = getTokensFromSpan(row);
|
||||
return isUndefined(v) ? '-' : v;
|
||||
},
|
||||
width: 78,
|
||||
},
|
||||
{
|
||||
title: I18n.t('db_add_table_field_type_time'),
|
||||
dataIndex: 'duration',
|
||||
render: formatDuration,
|
||||
width: 78,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={css['trace-table']}>
|
||||
<Table
|
||||
tableProps={{
|
||||
dataSource: spans,
|
||||
rowKey: 'span_id',
|
||||
columns,
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.full-template {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { IconCozIllusEmpty } from '@coze-arch/coze-design/illustrations';
|
||||
import { Spin } from '@coze-arch/coze-design';
|
||||
|
||||
import css from './template.module.less';
|
||||
|
||||
export const EmptyTemplate = () => (
|
||||
<div className={css['full-template']}>
|
||||
<IconCozIllusEmpty width="100px" height="100px" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const LoadingTemplate = () => (
|
||||
<div className={css['full-template']}>
|
||||
<Spin />
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { workflowApi } from '@coze-workflow/base';
|
||||
import { type TraceFrontendSpan } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { sortSpans } from '../../utils';
|
||||
import { useTraceListStore } from '../../contexts';
|
||||
import { MAX_TRACE_TIME } from '../../constants';
|
||||
|
||||
export const useTrace = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [spans, setSpans] = useState<TraceFrontendSpan[] | null>(null);
|
||||
|
||||
const { span } = useTraceListStore(store => ({
|
||||
span: store.span,
|
||||
}));
|
||||
|
||||
const fetch = useMemoizedFn(async (logId: string) => {
|
||||
setLoading(true);
|
||||
/** 查询日志时,开始结束时间必传,由于用户可查范围为 7 天内,所以直接伪造 7 天时间间隔即可 */
|
||||
const now = dayjs().endOf('day').valueOf();
|
||||
const end = dayjs()
|
||||
.subtract(MAX_TRACE_TIME, 'day')
|
||||
.startOf('day')
|
||||
.valueOf();
|
||||
|
||||
try {
|
||||
const { data } = await workflowApi.GetTraceSDK({
|
||||
log_id: logId,
|
||||
start_at: end,
|
||||
end_at: now,
|
||||
});
|
||||
if (!data || !data.spans) {
|
||||
return;
|
||||
}
|
||||
const next = sortSpans(data.spans);
|
||||
setSpans(next);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (span?.log_id) {
|
||||
fetch(span.log_id);
|
||||
}
|
||||
}, [span, fetch]);
|
||||
|
||||
return { spans, loading };
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
.trace-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
.trace-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: var(--coz-fg-hglt);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.header-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
import { TraceSelect } from '../trace-select';
|
||||
|
||||
import css from './header.module.less';
|
||||
|
||||
export const TraceListPanelHeader: React.FC = () => (
|
||||
<div className={css['trace-panel-header']}>
|
||||
<div className={css['header-tabs']}>
|
||||
<div className={css['trace-title']}>{I18n.t('debug_btn')}</div>
|
||||
</div>
|
||||
<TraceSelect />
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { TraceListPanel } from './list-panel';
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { BottomPanel } from '@coze-workflow/test-run-shared';
|
||||
import { type TraceFrontendSpan } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { TraceGraph } from '../trace-graph';
|
||||
import { type GotoParams } from '../../types';
|
||||
import { TraceListProvider } from '../../contexts';
|
||||
import { TraceListPanelHeader } from './header';
|
||||
|
||||
interface TraceListPanelProps {
|
||||
spaceId: string;
|
||||
workflowId: string;
|
||||
maxHeight: number;
|
||||
isInOp?: boolean;
|
||||
onOpenDetail: (span: TraceFrontendSpan) => void;
|
||||
onClose: () => void;
|
||||
onGotoNode: (params: GotoParams) => void;
|
||||
}
|
||||
|
||||
export const TraceListPanel: React.FC<TraceListPanelProps> = ({
|
||||
spaceId,
|
||||
workflowId,
|
||||
isInOp,
|
||||
maxHeight,
|
||||
onOpenDetail,
|
||||
onGotoNode,
|
||||
onClose,
|
||||
}) => (
|
||||
<TraceListProvider spaceId={spaceId} workflowId={workflowId} isInOp={isInOp}>
|
||||
<BottomPanel
|
||||
header={<TraceListPanelHeader />}
|
||||
height={300}
|
||||
resizable={{
|
||||
min: 300,
|
||||
max: maxHeight,
|
||||
}}
|
||||
onClose={onClose}
|
||||
>
|
||||
<TraceGraph onOpenDetail={onOpenDetail} onGotoNode={onGotoNode} />
|
||||
</BottomPanel>
|
||||
</TraceListProvider>
|
||||
);
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { IconCozCalendar } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
DatePicker as DatePickerCore,
|
||||
type DatePickerProps as DatePickerCoreProps,
|
||||
IconButton,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
type DatePickerProps = Pick<DatePickerCoreProps, 'value'> & {
|
||||
onChange: (v: [Date, Date]) => void;
|
||||
};
|
||||
|
||||
export const DatePicker: React.FC<DatePickerProps> = ({
|
||||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const disabledDate = (date?: Date) => {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
const current = date.getTime();
|
||||
const end = dayjs().endOf('day').valueOf();
|
||||
const start = dayjs().subtract(6, 'day').startOf('day').valueOf();
|
||||
|
||||
return current < start || current > end;
|
||||
};
|
||||
|
||||
const triggerRender = useCallback(
|
||||
() => (
|
||||
<IconButton icon={<IconCozCalendar />} color="secondary" size="small" />
|
||||
),
|
||||
[],
|
||||
);
|
||||
const handleChange = (v: any) => {
|
||||
onChange(v);
|
||||
};
|
||||
|
||||
return (
|
||||
<DatePickerCore
|
||||
type="dateRange"
|
||||
triggerRender={triggerRender}
|
||||
disabledDate={disabledDate}
|
||||
onChange={handleChange}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { TraceSelect } from './trace-select';
|
||||
@@ -0,0 +1,24 @@
|
||||
.select-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
justify-content: space-between;
|
||||
column-gap: 8px;
|
||||
padding: 0 8px;
|
||||
|
||||
.time {
|
||||
width: 128px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 242px;
|
||||
column-gap: 4px;
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { gotoDebugFlow } from '@coze-workflow/test-run-shared';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { type Span } from '@coze-arch/bot-api/workflow_api';
|
||||
import { IconCozExit } from '@coze-arch/coze-design/icons';
|
||||
import {
|
||||
Typography,
|
||||
Tag,
|
||||
IconButton,
|
||||
type ButtonProps,
|
||||
} from '@coze-arch/coze-design';
|
||||
|
||||
import { StatusIcon } from '../status-tag';
|
||||
import {
|
||||
getTimeFromSpan,
|
||||
isTriggerFromSpan,
|
||||
getGotoNodeParams,
|
||||
} from '../../utils';
|
||||
import { useTraceListStore } from '../../contexts';
|
||||
|
||||
import css from './select-option.module.less';
|
||||
|
||||
interface SelectOptionProps {
|
||||
span: Span;
|
||||
}
|
||||
|
||||
export const SelectOption: React.FC<SelectOptionProps> = ({ span }) => {
|
||||
const time = useMemo(() => getTimeFromSpan(span), [span]);
|
||||
const isTrigger = useMemo(() => isTriggerFromSpan(span), [span]);
|
||||
const { spaceId, isInOp } = useTraceListStore(store => ({
|
||||
spaceId: store.spaceId,
|
||||
isInOp: store.isInOp,
|
||||
}));
|
||||
const jumpToDebugFlow: ButtonProps['onClick'] = e => {
|
||||
e.stopPropagation();
|
||||
const params = getGotoNodeParams(span);
|
||||
gotoDebugFlow(
|
||||
{
|
||||
...params,
|
||||
spaceId,
|
||||
},
|
||||
isInOp,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={css['select-option']}>
|
||||
<div className={css.title}>
|
||||
<StatusIcon status={span.status_code} className={css.icon} />
|
||||
<Typography.Text ellipsis={{ showTooltip: true }}>
|
||||
{time}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
{isTrigger ? (
|
||||
<Tag
|
||||
style={{
|
||||
color: 'var(--coz-fg-hglt)',
|
||||
backgroundColor: 'var(--coz-mg-hglt)',
|
||||
}}
|
||||
size={'mini'}
|
||||
>
|
||||
{I18n.t('workflow_start_trigger_triggername')}
|
||||
</Tag>
|
||||
) : null}
|
||||
<IconButton
|
||||
size="mini"
|
||||
icon={<IconCozExit />}
|
||||
onClick={jumpToDebugFlow}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
import { IconCozFilter } from '@coze-arch/coze-design/icons';
|
||||
import { Select, IconButton } from '@coze-arch/coze-design';
|
||||
import { SpanStatus } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
interface StatusSelectProps {
|
||||
value: SpanStatus;
|
||||
onChange: (v: SpanStatus) => void;
|
||||
}
|
||||
|
||||
export const StatusSelect: React.FC<StatusSelectProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const triggerRender = useCallback(
|
||||
() => (
|
||||
<IconButton icon={<IconCozFilter />} color="secondary" size="small" />
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={value}
|
||||
triggerRender={triggerRender}
|
||||
optionList={[
|
||||
{
|
||||
value: SpanStatus.Unknown,
|
||||
label: I18n.t('query_status_all'),
|
||||
},
|
||||
{
|
||||
value: SpanStatus.Fail,
|
||||
label: I18n.t('query_status_failed'),
|
||||
},
|
||||
{
|
||||
value: SpanStatus.Success,
|
||||
label: I18n.t('query_status_completed'),
|
||||
},
|
||||
]}
|
||||
onChange={onChange as any}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
.trace-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.trace-filter {
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 4px;
|
||||
padding: 0 4px;
|
||||
border: 1px solid var(--coz-stroke-plus);
|
||||
border-right: 0;
|
||||
border-top-left-radius: 8px;
|
||||
border-bottom-left-radius: 8px;
|
||||
}
|
||||
|
||||
.main-select {
|
||||
width: 200px;
|
||||
border-top-left-radius: 0px!important;
|
||||
border-bottom-left-radius: 0px!important;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { Select } from '@coze-arch/coze-design';
|
||||
|
||||
import { getTimeFromSpan } from '../../utils';
|
||||
import { useTraceListStore } from '../../contexts';
|
||||
import { useOptions } from './use-options';
|
||||
import { StatusSelect } from './status-select';
|
||||
import { SelectOption } from './select-option';
|
||||
import { DatePicker } from './date-picker';
|
||||
|
||||
import css from './trace-select.module.less';
|
||||
|
||||
export const TraceSelect: React.FC = () => {
|
||||
const { span, workflowId, patch } = useTraceListStore(store => ({
|
||||
span: store.span,
|
||||
workflowId: store.workflowId,
|
||||
patch: store.patch,
|
||||
}));
|
||||
|
||||
const {
|
||||
date,
|
||||
status,
|
||||
setStatus,
|
||||
options,
|
||||
optionsCacheRef,
|
||||
fetch,
|
||||
onDateChange,
|
||||
} = useOptions(workflowId);
|
||||
|
||||
const optionList = useMemo(() => {
|
||||
const temp = options.map(i => ({
|
||||
...i,
|
||||
value: i.log_id,
|
||||
label: <SelectOption span={i} />,
|
||||
}));
|
||||
return temp;
|
||||
}, [options]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(v: any) => {
|
||||
patch({
|
||||
span:
|
||||
v && optionsCacheRef.current.has(v)
|
||||
? optionsCacheRef.current.get(v)
|
||||
: null,
|
||||
});
|
||||
},
|
||||
[optionsCacheRef, patch],
|
||||
);
|
||||
|
||||
const handleDropdownVisibleChange = useMemoizedFn((v: boolean) => {
|
||||
if (v) {
|
||||
fetch();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={css['trace-select']}>
|
||||
<div className={css['trace-filter']}>
|
||||
<DatePicker value={date} onChange={onDateChange} />
|
||||
<StatusSelect value={status} onChange={setStatus} />
|
||||
</div>
|
||||
|
||||
<Select
|
||||
className={css['main-select']}
|
||||
optionList={optionList}
|
||||
value={span?.log_id}
|
||||
onChange={handleChange}
|
||||
renderSelectedItem={e => {
|
||||
const current = optionsCacheRef.current.get(e.value);
|
||||
return current ? getTimeFromSpan(current) : '-';
|
||||
}}
|
||||
onDropdownVisibleChange={handleDropdownVisibleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable complexity */
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { useMemoizedFn } from 'ahooks';
|
||||
import { workflowApi } from '@coze-workflow/base';
|
||||
import { SpanStatus, type Span } from '@coze-arch/bot-api/workflow_api';
|
||||
|
||||
import { useTraceListStore } from '../../contexts';
|
||||
import { MAX_TRACE_LENGTH, MAX_TRACE_TIME } from '../../constants';
|
||||
|
||||
const getDefaultDate = (): [Date, Date] => {
|
||||
const end = dayjs().endOf('day').toDate();
|
||||
const start = dayjs()
|
||||
.subtract(MAX_TRACE_TIME - 1, 'day')
|
||||
.startOf('day')
|
||||
.toDate();
|
||||
|
||||
return [start, end];
|
||||
};
|
||||
|
||||
export const useOptions = (workflowId: string) => {
|
||||
const [date, setDate] = useState<[Date, Date]>(getDefaultDate());
|
||||
const [status, setStatus] = useState<SpanStatus>(SpanStatus.Unknown);
|
||||
const [options, setOptions] = useState<Span[]>([]);
|
||||
|
||||
const optionsCacheRef = useRef(new Map<string, Span>());
|
||||
|
||||
const { ready, span, patch } = useTraceListStore(store => ({
|
||||
span: store.span,
|
||||
ready: store.ready,
|
||||
patch: store.patch,
|
||||
}));
|
||||
|
||||
const fetch = useMemoizedFn(async () => {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const executeMode = searchParams.get('execute_mode');
|
||||
const executeId = searchParams.get('execute_id');
|
||||
|
||||
const { spans } = await workflowApi.ListRootSpans({
|
||||
workflow_id: workflowId,
|
||||
limit: MAX_TRACE_LENGTH,
|
||||
offset: 0,
|
||||
start_at: date[0].getTime(),
|
||||
end_at: date[1].getTime(),
|
||||
status: status === SpanStatus.Unknown ? undefined : status,
|
||||
execute_mode: executeMode ? Number(executeMode) : undefined,
|
||||
});
|
||||
const next = spans || [];
|
||||
let maybeInitialSpan = next[0];
|
||||
if (executeId && !ready && !span) {
|
||||
try {
|
||||
const { data } = await workflowApi.GetTraceSDK({
|
||||
execute_id: executeId,
|
||||
workflow_id: workflowId,
|
||||
start_at: date[0].getTime(),
|
||||
end_at: date[1].getTime(),
|
||||
});
|
||||
const first = data?.spans?.[0];
|
||||
if (first?.log_id) {
|
||||
maybeInitialSpan = first;
|
||||
const urlSpan = next.find(i => i.log_id === first.log_id);
|
||||
if (!urlSpan) {
|
||||
next.unshift(first);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line @coze-arch/no-empty-catch -- 无需报错
|
||||
} catch {
|
||||
// 无需报错
|
||||
}
|
||||
}
|
||||
next.forEach(s => {
|
||||
if (s.log_id) {
|
||||
optionsCacheRef.current.set(s.log_id, s);
|
||||
}
|
||||
});
|
||||
setOptions(next);
|
||||
// 如果没有初始化,就初始化一次
|
||||
if (!ready && !span && maybeInitialSpan) {
|
||||
patch({ span: maybeInitialSpan });
|
||||
}
|
||||
if (!ready) {
|
||||
patch({ ready: true });
|
||||
}
|
||||
});
|
||||
|
||||
const handleDateChange = useCallback(
|
||||
(next: [Date, Date]) => {
|
||||
const [start, end] = next;
|
||||
// 时间选择器选择的日期是当天 0 点,需要转化为当天 11 点 59 分 59 秒
|
||||
setDate([start, dayjs(end).endOf('day').toDate()] as [Date, Date]);
|
||||
},
|
||||
[setDate],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetch();
|
||||
}, [date, status, fetch]);
|
||||
|
||||
return {
|
||||
date,
|
||||
status,
|
||||
setStatus,
|
||||
options,
|
||||
optionsCacheRef,
|
||||
fetch,
|
||||
onDateChange: handleDateChange,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user