182 lines
5.5 KiB
TypeScript
182 lines
5.5 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import React from 'react';
|
|
import { VList, VListHandle } from 'virtua';
|
|
import { PlayerContext } from 'App/components/Session/playerContext';
|
|
import { processLog, UnifiedLog } from './utils';
|
|
import { observer } from 'mobx-react-lite';
|
|
import { useStore } from 'App/mstore';
|
|
import {
|
|
ServiceName,
|
|
serviceNames,
|
|
} from 'App/components/Client/Integrations/apiMethods';
|
|
import BottomBlock from 'App/components/shared/DevTools/BottomBlock';
|
|
import { capitalize } from 'App/utils';
|
|
import { Icon } from 'UI';
|
|
import { Segmented, Input, Tooltip } from 'antd';
|
|
import { SearchOutlined } from '@ant-design/icons';
|
|
import { client } from 'App/mstore';
|
|
import { FailedFetch, LoadingFetch } from './StatusMessages';
|
|
import { TableHeader, LogRow } from './Table';
|
|
|
|
async function fetchLogs(
|
|
tab: string,
|
|
projectId: string,
|
|
sessionId: string
|
|
): Promise<UnifiedLog[]> {
|
|
const data = await client.get(
|
|
`/integrations/v1/integrations/${tab}/${projectId}/data/${sessionId}`
|
|
);
|
|
const json = await data.json();
|
|
try {
|
|
const logsResp = await fetch(json.url);
|
|
if (logsResp.ok) {
|
|
const logJson = await logsResp.json();
|
|
if (logJson.length === 0) return [];
|
|
return processLog(logJson);
|
|
} else {
|
|
throw new Error('Failed to fetch logs');
|
|
}
|
|
} catch (e) {
|
|
console.log(e);
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
function BackendLogsPanel() {
|
|
const { projectsStore, sessionStore, integrationsStore } = useStore();
|
|
const integratedServices =
|
|
integrationsStore.integrations.backendLogIntegrations;
|
|
const defaultTab = integratedServices[0]!.name;
|
|
const sessionId = sessionStore.currentId;
|
|
const projectId = projectsStore.siteId!;
|
|
const [tab, setTab] = React.useState<ServiceName>(defaultTab as ServiceName);
|
|
const { data, isError, isPending, isSuccess, refetch } = useQuery<
|
|
UnifiedLog[]
|
|
>({
|
|
queryKey: ['integrationLogs', tab, sessionId],
|
|
staleTime: 1000 * 30,
|
|
queryFn: () => fetchLogs(tab!, projectId, sessionId),
|
|
enabled: tab !== null,
|
|
retry: 3,
|
|
});
|
|
const [filter, setFilter] = React.useState('');
|
|
|
|
const onFilterChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setFilter(e.target.value);
|
|
};
|
|
|
|
const tabs = Object.entries(serviceNames)
|
|
.filter(
|
|
([slug]) => integratedServices.findIndex((i) => i.name === slug) !== -1
|
|
)
|
|
.map(([slug, name]) => ({
|
|
label: (
|
|
<div className={'flex items-center gap-2'}>
|
|
<Icon size={14} name={`integrations/${slug}`} /> <div>{name}</div>
|
|
</div>
|
|
),
|
|
value: slug,
|
|
}));
|
|
|
|
return (
|
|
<BottomBlock style={{ height: '100%' }}>
|
|
<BottomBlock.Header>
|
|
<div className="flex items-center justify-between w-full">
|
|
<div className={'flex gap-2 items-center'}>
|
|
<div className={'font-semibold'}>Traces</div>
|
|
{tabs.length && tab ? (
|
|
<div>
|
|
<Segmented
|
|
options={tabs}
|
|
value={tab}
|
|
onChange={setTab}
|
|
size="small"
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Segmented
|
|
options={[
|
|
{ label: 'All Tabs', value: 'all' },
|
|
{
|
|
label: (
|
|
<Tooltip title="Backend logs are fetched for all tabs combined.">
|
|
<span>Current Tab</span>
|
|
</Tooltip>
|
|
),
|
|
value: 'current',
|
|
disabled: true,
|
|
},
|
|
]}
|
|
defaultValue="all"
|
|
size="small"
|
|
className="rounded-full font-medium"
|
|
/>
|
|
|
|
<Input
|
|
className="rounded-lg"
|
|
placeholder="Filter by keyword"
|
|
name="filter"
|
|
onChange={onFilterChange}
|
|
value={filter}
|
|
size="small"
|
|
prefix={<SearchOutlined className="text-neutral-400" />}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</BottomBlock.Header>
|
|
|
|
<BottomBlock.Content className="overflow-y-auto">
|
|
{isPending ? <LoadingFetch provider={capitalize(tab)} /> : null}
|
|
{isError ? (
|
|
<FailedFetch provider={capitalize(tab)} onRetry={refetch} />
|
|
) : null}
|
|
{isSuccess ? <LogsTable data={data} /> : null}
|
|
</BottomBlock.Content>
|
|
</BottomBlock>
|
|
);
|
|
}
|
|
|
|
const LogsTable = observer(({ data }: { data: UnifiedLog[] }) => {
|
|
const { store, player } = React.useContext(PlayerContext);
|
|
const time = store.get().time;
|
|
const sessionStart = store.get().sessionStart;
|
|
const _list = React.useRef<VListHandle>(null);
|
|
const activeIndex = React.useMemo(() => {
|
|
const currTs = time + sessionStart;
|
|
const index = data.findIndex((log) =>
|
|
log.timestamp !== 'N/A'
|
|
? new Date(log.timestamp).getTime() >= currTs
|
|
: false
|
|
);
|
|
return index === -1 ? data.length - 1 : index;
|
|
}, [time, data.length]);
|
|
React.useEffect(() => {
|
|
if (_list.current) {
|
|
_list.current.scrollToIndex(activeIndex);
|
|
}
|
|
}, [activeIndex]);
|
|
|
|
const onJump = (ts: number) => {
|
|
player.jump(ts - sessionStart);
|
|
};
|
|
return (
|
|
<>
|
|
<TableHeader size={data.length} />
|
|
<VList ref={_list} count={data.length}>
|
|
{data.map((log, index) => (
|
|
<LogRow
|
|
key={index}
|
|
isActive={index === activeIndex}
|
|
log={log}
|
|
onJump={onJump}
|
|
/>
|
|
))}
|
|
</VList>
|
|
</>
|
|
);
|
|
});
|
|
|
|
export default observer(BackendLogsPanel);
|