+ }`}
+ >
{/* Sidebar component, swap this element with another sidebar if you like */}
@@ -304,7 +320,8 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold text-gray-700'
)}
target={item.linkType === 'external' ? '_blank' : undefined}
- rel={item.linkType === 'external' ? 'noreferrer' : undefined}>
+ rel={item.linkType === 'external' ? 'noreferrer' : undefined}
+ >
{
? 'bg-gray-50'
: 'hover:bg-gray-50',
'flex items-center w-full text-left rounded-md p-2 gap-x-3 text-sm leading-6 font-semibold text-gray-700'
- )}>
+ )}
+ >
{
}
rel={
subItem.linkType === 'external' ? 'noreferrer' : undefined
- }>
+ }
+ >
{subItem.name}
@@ -377,17 +396,17 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
!sidebarOpen
? 'lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-8 lg:flex-col justify-end lg:py-2 lg:px-1'
: ''
- }`}>
+ }`}
+ >
- {/* This is a bit hacky -- just quickly prototyping and these margins were the ones that worked! */}
-
-
+
+
-
diff --git a/telemetry/ui/src/components/nav/breadcrumb.tsx b/telemetry/ui/src/components/nav/breadcrumb.tsx
index d2da9d14e..2021af606 100644
--- a/telemetry/ui/src/components/nav/breadcrumb.tsx
+++ b/telemetry/ui/src/components/nav/breadcrumb.tsx
@@ -62,8 +62,7 @@ export const BreadCrumb = () => {
className="h-5 w-5 flex-shrink-0 text-gray-300"
fill="currentColor"
viewBox="0 0 20 20"
- aria-hidden="true"
- >
+ aria-hidden="true">
{isNullPK ? (
@@ -72,8 +71,7 @@ export const BreadCrumb = () => {
+ aria-current={page.current ? 'page' : undefined}>
{decodeURIComponent(page.name)}
)}
diff --git a/telemetry/ui/src/components/routes/AdminView.tsx b/telemetry/ui/src/components/routes/AdminView.tsx
index 287af2123..950f57c65 100644
--- a/telemetry/ui/src/components/routes/AdminView.tsx
+++ b/telemetry/ui/src/components/routes/AdminView.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import { useQuery } from 'react-query';
+import { useQuery } from '@tanstack/react-query';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../common/table';
import { DefaultService } from '../../api';
import { Loading } from '../common/loading';
@@ -53,13 +53,15 @@ const RecordsHeader = (props: {
export const AdminView = () => {
const [displayZeroCount, setDisplayZeroCount] = useState(false);
- const { data, isLoading } = useQuery(['indexingJobs', displayZeroCount], () =>
- DefaultService.getIndexingJobsApiV0IndexingJobsGet(
- 0, // TODO -- add pagination
- 100,
- !displayZeroCount
- )
- );
+ const { data, isLoading } = useQuery({
+ queryKey: ['indexingJobs', displayZeroCount],
+ queryFn: () =>
+ DefaultService.getIndexingJobsApiV0IndexingJobsGet(
+ 0, // TODO -- add pagination
+ 100,
+ !displayZeroCount
+ )
+ });
if (isLoading) {
return ;
}
diff --git a/telemetry/ui/src/components/routes/AppList.tsx b/telemetry/ui/src/components/routes/AppList.tsx
index 03d105f95..9023ca064 100644
--- a/telemetry/ui/src/components/routes/AppList.tsx
+++ b/telemetry/ui/src/components/routes/AppList.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import { useQuery } from 'react-query';
+import { useQuery } from '@tanstack/react-query';
import { Navigate, useParams } from 'react-router';
import { Loading } from '../common/loading';
import { ApplicationSummary, DefaultService } from '../../api';
@@ -309,17 +309,17 @@ export const AppList = () => {
const [searchParams] = useSearchParams();
const currentOffset = searchParams.get('offset') ? parseInt(searchParams.get('offset')!) : 0;
const pageSize = DEFAULT_LIMIT;
- const { data, error } = useQuery(
- ['apps', projectId, partitionKey, pageSize, currentOffset],
- () =>
+ const { data, error } = useQuery({
+ queryKey: ['apps', projectId, partitionKey, pageSize, currentOffset],
+ queryFn: () =>
DefaultService.getAppsApiV0ProjectIdPartitionKeyAppsGet(
projectId as string,
partitionKey ? partitionKey : '__none__',
pageSize,
currentOffset
),
- { enabled: projectId !== undefined }
- );
+ enabled: projectId !== undefined
+ });
const [queriedData, setQueriedData] = useState(data);
diff --git a/telemetry/ui/src/components/routes/CompareView.tsx b/telemetry/ui/src/components/routes/CompareView.tsx
new file mode 100644
index 000000000..2d331add0
--- /dev/null
+++ b/telemetry/ui/src/components/routes/CompareView.tsx
@@ -0,0 +1,195 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 { useState } from 'react';
+import { useParams } from 'react-router-dom';
+import { useQuery } from '@tanstack/react-query';
+import { DefaultService } from '../../api';
+import { Loading } from '../common/loading';
+import { GraphView } from './app/GraphView';
+import { TwoColumnLayout } from '../common/layout';
+
+const CompareSelector = (props: {
+ projectId: string;
+ selected: string | null;
+ onSelect: (appId: string, partitionKey: string | null) => void;
+ label: string;
+}) => {
+ const { data } = useQuery({
+ queryKey: ['apps', props.projectId, '__none__'],
+ queryFn: () =>
+ DefaultService.getAppsApiV0ProjectIdPartitionKeyAppsGet(props.projectId, '__none__', 100)
+ });
+
+ return (
+
+
+
+
+ );
+};
+
+export const CompareView = () => {
+ const { projectId } = useParams<{ projectId: string }>();
+ const [leftApp, setLeftApp] = useState<{ appId: string; partitionKey: string | null } | null>(
+ null
+ );
+ const [rightApp, setRightApp] = useState<{ appId: string; partitionKey: string | null } | null>(
+ null
+ );
+
+ const { data: leftData, isLoading: leftLoading } = useQuery({
+ queryKey: ['steps', leftApp?.appId, leftApp?.partitionKey],
+ queryFn: () =>
+ DefaultService.getApplicationLogsApiV0ProjectIdAppIdPartitionKeyAppsGet(
+ projectId!,
+ leftApp!.appId,
+ leftApp!.partitionKey || '__none__'
+ ),
+ enabled: !!leftApp
+ });
+
+ const { data: rightData, isLoading: rightLoading } = useQuery({
+ queryKey: ['steps', rightApp?.appId, rightApp?.partitionKey],
+ queryFn: () =>
+ DefaultService.getApplicationLogsApiV0ProjectIdAppIdPartitionKeyAppsGet(
+ projectId!,
+ rightApp!.appId,
+ rightApp!.partitionKey || '__none__'
+ ),
+ enabled: !!rightApp
+ });
+
+ // Find divergence point
+ const divergenceIndex =
+ leftData && rightData
+ ? (() => {
+ const leftSteps = [...leftData.steps].sort(
+ (a, b) => a.step_start_log.sequence_id - b.step_start_log.sequence_id
+ );
+ const rightSteps = [...rightData.steps].sort(
+ (a, b) => a.step_start_log.sequence_id - b.step_start_log.sequence_id
+ );
+ const minLen = Math.min(leftSteps.length, rightSteps.length);
+ for (let i = 0; i < minLen; i++) {
+ if (leftSteps[i].step_start_log.action !== rightSteps[i].step_start_log.action) {
+ return i;
+ }
+ }
+ return leftSteps.length !== rightSteps.length ? minLen : -1;
+ })()
+ : -1;
+
+ return (
+
+
Compare Runs
+
+
+ setLeftApp({ appId, partitionKey: pk })}
+ label="Run A"
+ />
+ setRightApp({ appId, partitionKey: pk })}
+ label="Run B"
+ />
+
+
+ {divergenceIndex >= 0 && (
+
+ Divergence at step {divergenceIndex}: Run A executes{' '}
+
+ {leftData?.steps.sort(
+ (a, b) => a.step_start_log.sequence_id - b.step_start_log.sequence_id
+ )[divergenceIndex]?.step_start_log.action || '?'}
+
+ , Run B executes{' '}
+
+ {rightData?.steps.sort(
+ (a, b) => a.step_start_log.sequence_id - b.step_start_log.sequence_id
+ )[divergenceIndex]?.step_start_log.action || '?'}
+
+
+ )}
+
+ {(leftLoading || rightLoading) &&
}
+
+ {leftData && rightData && (
+
+
+
+ Run A: {leftApp?.appId}
+
+
+
+
+
+ }
+ secondItem={
+
+
+ Run B: {rightApp?.appId}
+
+
+
+
+
+ }
+ />
+
+ )}
+
+ {!leftApp && !rightApp && (
+
+ Select two runs to compare
+
+ )}
+
+ );
+};
diff --git a/telemetry/ui/src/components/routes/HomeView.tsx b/telemetry/ui/src/components/routes/HomeView.tsx
new file mode 100644
index 000000000..5798896e0
--- /dev/null
+++ b/telemetry/ui/src/components/routes/HomeView.tsx
@@ -0,0 +1,303 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 { Link } from 'react-router-dom';
+import { useQuery } from '@tanstack/react-query';
+import { DefaultService } from '../../api';
+import Joyride, { CallBackProps, STATUS, Step } from 'react-joyride';
+import {
+ Square2StackIcon,
+ MagnifyingGlassIcon,
+ WrenchScrewdriverIcon,
+ FolderOpenIcon,
+ RocketLaunchIcon,
+ AcademicCapIcon,
+ SparklesIcon,
+ BookOpenIcon
+} from '@heroicons/react/24/outline';
+
+const TOUR_COMPLETED_KEY = 'burr_tour_completed';
+
+const tourSteps: Step[] = [
+ {
+ target: '[data-tour="sidebar-projects"]',
+ content: 'Start here. Projects contain all your tracked Burr application runs.',
+ disableBeacon: true,
+ placement: 'right'
+ },
+ {
+ target: '[data-tour="sidebar-search"]',
+ content: 'Search across actions, state keys, and conditions in your projects.',
+ placement: 'right'
+ },
+ {
+ target: '[data-tour="sidebar-builder"]',
+ content:
+ 'Build state machine graphs visually. Define reads/writes per node and generate Python code.',
+ placement: 'right'
+ },
+ {
+ target: '[data-tour="sidebar-demos"]',
+ content:
+ 'Interactive demos with live telemetry. Try them to see how Burr tracking works with real data.',
+ placement: 'right'
+ },
+ {
+ target: '[data-tour="start-section"]',
+ content: 'Quick actions to jump into the most common workflows.',
+ placement: 'bottom'
+ },
+ {
+ target: '[data-tour="recent-section"]',
+ content: 'Your recent projects appear here for quick access.',
+ placement: 'top'
+ }
+];
+
+const StartAction = (props: {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ icon: React.ComponentType;
+ label: string;
+ href: string;
+ description: string;
+}) => (
+
+
+
+
{props.label}
+
{props.description}
+
+
+);
+
+const WalkthroughCard = (props: {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ icon: React.ComponentType;
+ title: string;
+ description: string;
+ onClick: () => void;
+ badge?: string;
+ iconColor: string;
+}) => (
+
+);
+
+export const HomeView = () => {
+ const [runTour, setRunTour] = useState(false);
+ const [showWelcome, setShowWelcome] = useState(true);
+
+ const { data: projects } = useQuery({
+ queryKey: ['projects'],
+ queryFn: () => DefaultService.getProjectsApiV0ProjectsGet()
+ });
+
+ const recentProjects = (projects || [])
+ .sort((a, b) => new Date(b.last_written).getTime() - new Date(a.last_written).getTime())
+ .slice(0, 5);
+
+ useEffect(() => {
+ const tourDone = localStorage.getItem(TOUR_COMPLETED_KEY);
+ if (!tourDone) {
+ const timer = setTimeout(() => setRunTour(true), 800);
+ return () => clearTimeout(timer);
+ }
+ }, []);
+
+ const handleTourCallback = (data: CallBackProps) => {
+ const { status } = data;
+ if (status === STATUS.FINISHED || status === STATUS.SKIPPED) {
+ setRunTour(false);
+ localStorage.setItem(TOUR_COMPLETED_KEY, 'true');
+ }
+ };
+
+ const handleShowWelcomeChange = (checked: boolean) => {
+ setShowWelcome(checked);
+ if (!checked) {
+ localStorage.setItem('burr_hide_welcome', 'true');
+ } else {
+ localStorage.removeItem('burr_hide_welcome');
+ }
+ };
+
+ return (
+
+
+
+ {/* Header */}
+
+
+

+
+
Apache Burr
+
Observability for state machines
+
+
+
+
+
+ {/* Left column: Start + Recent */}
+
+
+
+
+
Recent
+ {recentProjects.length > 0 ? (
+
+ {recentProjects.map((project) => (
+
+ {project.name}
+
+ {new Date(project.last_written).toLocaleDateString()}
+
+
+ ))}
+
+ ) : (
+
+ No projects yet. Run a Burr application with tracking enabled.
+
+ )}
+
+
+
+ {/* Right column: Walkthroughs */}
+
+
Walkthroughs
+
+ {
+ localStorage.removeItem(TOUR_COMPLETED_KEY);
+ setRunTour(true);
+ }}
+ iconColor="bg-dwlightblue"
+ />
+ (window.location.href = '/demos/counter')}
+ badge="New"
+ iconColor="bg-purple-500"
+ />
+ (window.location.href = '/builder')}
+ badge="New"
+ iconColor="bg-green-500"
+ />
+ window.open('https://burr.apache.org', '_blank')}
+ iconColor="bg-gray-500"
+ />
+
+
+
+
+ {/* Footer: pinned to bottom */}
+
+
+
+
+ );
+};
diff --git a/telemetry/ui/src/components/routes/ProjectList.tsx b/telemetry/ui/src/components/routes/ProjectList.tsx
index 8ccabc836..cb96f0b3a 100644
--- a/telemetry/ui/src/components/routes/ProjectList.tsx
+++ b/telemetry/ui/src/components/routes/ProjectList.tsx
@@ -17,7 +17,7 @@
* under the License.
*/
-import { useQuery } from 'react-query';
+import { useQuery } from '@tanstack/react-query';
import { DefaultService, Project } from '../../api';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../common/table';
import { Loading } from '../common/loading';
@@ -114,12 +114,17 @@ export const ProjectListTable = (props: { projects: Project[]; includeAnnotation
* Container for the table -- fetches the data and passes it to the table
*/
export const ProjectList = () => {
- const { data, error } = useQuery('projects', DefaultService.getProjectsApiV0ProjectsGet);
- const { data: backendSpec } = useQuery(['backendSpec'], () =>
- DefaultService.getAppSpecApiV0MetadataAppSpecGet().then((response) => {
- return response;
- })
- );
+ const { data, error } = useQuery({
+ queryKey: ['projects'],
+ queryFn: DefaultService.getProjectsApiV0ProjectsGet
+ });
+ const { data: backendSpec } = useQuery({
+ queryKey: ['backendSpec'],
+ queryFn: () =>
+ DefaultService.getAppSpecApiV0MetadataAppSpecGet().then((response) => {
+ return response;
+ })
+ });
if (error) return Error loading projects
;
if (data === undefined || backendSpec === undefined) return ;
return (
diff --git a/telemetry/ui/src/components/routes/SearchView.tsx b/telemetry/ui/src/components/routes/SearchView.tsx
new file mode 100644
index 000000000..eff04b58e
--- /dev/null
+++ b/telemetry/ui/src/components/routes/SearchView.tsx
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 { useState, useMemo } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { DefaultService } from '../../api';
+import Fuse from 'fuse.js';
+import { Link } from 'react-router-dom';
+import { Chip } from '../common/chip';
+import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
+
+type SearchResult = {
+ type: 'action' | 'state_key' | 'condition';
+ name: string;
+ context: string;
+ projectId: string;
+};
+
+export const SearchView = () => {
+ const [query, setQuery] = useState('');
+
+ const { data: projects } = useQuery({
+ queryKey: ['projects'],
+ queryFn: () => DefaultService.getProjectsApiV0ProjectsGet()
+ });
+
+ const searchItems = useMemo(() => {
+ if (!projects) return [];
+ const items: SearchResult[] = [];
+
+ for (const project of projects) {
+ // Index project names
+ items.push({
+ type: 'action',
+ name: project.name,
+ context: `Project: ${project.id}`,
+ projectId: project.id
+ });
+ }
+
+ return items;
+ }, [projects]);
+
+ const fuse = useMemo(
+ () =>
+ new Fuse(searchItems, {
+ keys: ['name', 'context'],
+ threshold: 0.3,
+ includeScore: true
+ }),
+ [searchItems]
+ );
+
+ const results = useMemo(() => {
+ if (!query.trim()) return searchItems.slice(0, 20);
+ return fuse.search(query, { limit: 50 }).map((r) => r.item);
+ }, [query, fuse, searchItems]);
+
+ const chipTypeMap: Record = {
+ action: 'action',
+ state_key: 'stateRead',
+ condition: 'stateWrite'
+ };
+
+ return (
+
+
+
+ setQuery(e.target.value)}
+ className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-dwlightblue focus:border-transparent text-lg"
+ autoFocus
+ />
+
+
+
+ {results.length === 0 && query.trim() && (
+
No results found
+ )}
+ {results.map((result, i) => (
+
+
+
+ {result.name}
+ {result.context}
+
+
+ ))}
+
+
+ );
+};
diff --git a/telemetry/ui/src/components/routes/app/ActionView.tsx b/telemetry/ui/src/components/routes/app/ActionView.tsx
index bd5c5cabf..f03a266c6 100644
--- a/telemetry/ui/src/components/routes/app/ActionView.tsx
+++ b/telemetry/ui/src/components/routes/app/ActionView.tsx
@@ -36,8 +36,7 @@ export const CodeView = (props: { code: string }) => {
className="bg-dwdarkblue/100 hide-scrollbar"
wrapLines={true}
wrapLongLines={true}
- style={base16AteliersulphurpoolLight}
- >
+ style={base16AteliersulphurpoolLight}>
{props.code}
diff --git a/telemetry/ui/src/components/routes/app/AnnotationsView.tsx b/telemetry/ui/src/components/routes/app/AnnotationsView.tsx
index a8aa6b824..50c65ae9c 100644
--- a/telemetry/ui/src/components/routes/app/AnnotationsView.tsx
+++ b/telemetry/ui/src/components/routes/app/AnnotationsView.tsx
@@ -35,7 +35,7 @@ import { FaClipboardList, FaExternalLinkAlt, FaThumbsDown, FaThumbsUp } from 're
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../common/table';
import { Chip } from '../../common/chip';
import { Link } from 'react-router-dom';
-import { useMutation, useQuery } from 'react-query';
+import { useMutation, useQuery } from '@tanstack/react-query';
import { Loading } from '../../common/loading';
import {
ChevronDownIcon,
@@ -535,20 +535,18 @@ export const AnnotationsTable = (props: {
refetchAnnotations?: () => void;
}) => {
// Just in case we want to do live-updating, we need to pass it into the form...
- const updateAnnotationMutation = useMutation(
- (data: { annotationID: number; annotationData: AnnotationUpdate }) =>
+ const updateAnnotationMutation = useMutation({
+ mutationFn: (data: { annotationID: number; annotationData: AnnotationUpdate }) =>
DefaultService.updateAnnotationApiV0ProjectIdAnnotationIdUpdateAnnotationsPut(
props.projectId,
data.annotationID,
data.annotationData
),
- {
- onSuccess: () => {
- props.refetchAnnotations && props.refetchAnnotations();
- setCurrentlyEditingAnnotation(null); // We have to reset it somehow
- }
+ onSuccess: () => {
+ props.refetchAnnotations && props.refetchAnnotations();
+ setCurrentlyEditingAnnotation(null); // We have to reset it somehow
}
- );
+ });
const anyHavePartitionKey = props.annotations.some(
(annotation) => annotation.partition_key !== null
);
@@ -572,18 +570,16 @@ export const AnnotationsTable = (props: {
actionName: currentlyEditingAnnotation?.step_name || '',
spanId: currentlyEditingAnnotation?.span_id || null
};
- const { data: appData } = useQuery(
- ['steps', currentlyEditingAnnotation?.app_id],
- () =>
+ const { data: appData } = useQuery({
+ queryKey: ['steps', currentlyEditingAnnotation?.app_id],
+ queryFn: () =>
DefaultService.getApplicationLogsApiV0ProjectIdAppIdPartitionKeyAppsGet(
props.projectId,
currentlyEditingAnnotation?.app_id as string,
currentlyEditingAnnotation?.partition_key as string
),
- {
- enabled: currentlyEditingAnnotation !== null
- }
- );
+ enabled: currentlyEditingAnnotation !== null
+ });
// Either we're editing one that exists, or we're creating a new one...
// TODO -- simplify the logic here, we should have one path...
const annotationStep = currentlyEditingAnnotation
@@ -1229,16 +1225,19 @@ const AnnotationEditCreateForm = (props: {
*/
export const AnnotationsViewContainer = () => {
const { projectId } = useLocationParams();
- const { data: backendSpec } = useQuery(['backendSpec'], () =>
- DefaultService.getAppSpecApiV0MetadataAppSpecGet().then((response) => {
- return response;
- })
- );
+ const { data: backendSpec } = useQuery({
+ queryKey: ['backendSpec'],
+ queryFn: () =>
+ DefaultService.getAppSpecApiV0MetadataAppSpecGet().then((response) => {
+ return response;
+ })
+ });
// TODO -- use a skiptoken to bypass annotation loading if we don't need them
- const { data, refetch } = useQuery(['annotations', projectId], () =>
- DefaultService.getAnnotationsApiV0ProjectIdAnnotationsGet(projectId as string)
- );
+ const { data, refetch } = useQuery({
+ queryKey: ['annotations', projectId],
+ queryFn: () => DefaultService.getAnnotationsApiV0ProjectIdAnnotationsGet(projectId as string)
+ });
// dummy value as this will not be linked to if annotations are not supported
if (data === undefined || backendSpec === undefined) return
;
diff --git a/telemetry/ui/src/components/routes/app/AppView.tsx b/telemetry/ui/src/components/routes/app/AppView.tsx
index fa33328f3..3608b3e15 100644
--- a/telemetry/ui/src/components/routes/app/AppView.tsx
+++ b/telemetry/ui/src/components/routes/app/AppView.tsx
@@ -25,7 +25,7 @@ import {
AttributeModel,
DefaultService
} from '../../../api';
-import { useMutation, useQuery } from 'react-query';
+import { useMutation, useQuery } from '@tanstack/react-query';
import { Loading } from '../../common/loading';
import { ApplicationTable } from './StepList';
import { TwoColumnLayout, TwoRowLayout } from '../../common/layout';
@@ -250,73 +250,67 @@ export const AppView = (props: {
const [currentEditingAnnotationContext, setCurrentEditingAnnotationContext] = useState<
AnnotationEditingContext | undefined
>(undefined);
- const { data: backendSpec } = useQuery(['backendSpec'], () =>
- DefaultService.getAppSpecApiV0MetadataAppSpecGet().then((response) => {
- return response;
- })
- );
- const { data, error } = useQuery(
- ['steps', appID, partitionKey],
- () =>
+ const { data: backendSpec } = useQuery({
+ queryKey: ['backendSpec'],
+ queryFn: () =>
+ DefaultService.getAppSpecApiV0MetadataAppSpecGet().then((response) => {
+ return response;
+ })
+ });
+ const { data, error } = useQuery({
+ queryKey: ['steps', appID, partitionKey],
+ queryFn: () =>
DefaultService.getApplicationLogsApiV0ProjectIdAppIdPartitionKeyAppsGet(
projectId as string,
appID as string,
props.partitionKey !== null ? props.partitionKey : '__none__'
),
- {
- refetchInterval: autoRefresh ? REFRESH_INTERVAL : false,
- enabled: shouldQuery
- }
- );
+ refetchInterval: autoRefresh ? REFRESH_INTERVAL : false,
+ enabled: shouldQuery
+ });
- const { data: currentFocusStepsData } = useQuery(
- ['steps', currentFocusAppID, currentFocusPartitionKey],
- () =>
+ const { data: currentFocusStepsData } = useQuery({
+ queryKey: ['steps', currentFocusAppID, currentFocusPartitionKey],
+ queryFn: () =>
DefaultService.getApplicationLogsApiV0ProjectIdAppIdPartitionKeyAppsGet(
projectId as string,
currentFocusAppID as string,
currentFocusPartitionKey !== null ? currentFocusPartitionKey : '__none__'
),
- {
- refetchInterval: autoRefresh ? REFRESH_INTERVAL : false,
- enabled: currentFocusAppID !== appID && currentFocusAppID !== undefined
- }
- );
+ refetchInterval: autoRefresh ? REFRESH_INTERVAL : false,
+ enabled: currentFocusAppID !== appID && currentFocusAppID !== undefined
+ });
// TODO -- use a skiptoken to bypass annotation loading if we don't need them
- const { data: annotationsData, refetch: refetchAnnotationsData } = useQuery(
- ['annotations', appID, partitionKey],
- () =>
+ const { data: annotationsData, refetch: refetchAnnotationsData } = useQuery({
+ queryKey: ['annotations', appID, partitionKey],
+ queryFn: () =>
DefaultService.getAnnotationsApiV0ProjectIdAnnotationsGet(
projectId as string,
appID as string,
partitionKey !== null ? partitionKey : '__none__'
),
- {
- refetchInterval: autoRefresh ? REFRESH_INTERVAL : false,
- enabled: shouldQuery && props.allowAnnotations && backendSpec?.supports_annotations
- }
- );
+ refetchInterval: autoRefresh ? REFRESH_INTERVAL : false,
+ enabled: shouldQuery && props.allowAnnotations && backendSpec?.supports_annotations
+ });
- const { data: currentFocusAnnotationsData } = useQuery(
- ['annotations', currentFocusAppID, currentFocusPartitionKey],
- () =>
+ const { data: currentFocusAnnotationsData } = useQuery({
+ queryKey: ['annotations', currentFocusAppID, currentFocusPartitionKey],
+ queryFn: () =>
DefaultService.getAnnotationsApiV0ProjectIdAnnotationsGet(
projectId as string,
currentFocusAppID as string,
currentFocusPartitionKey !== null ? partitionKey : '__none__'
),
- {
- enabled:
- shouldQuery &&
- props.allowAnnotations &&
- backendSpec?.supports_annotations &&
- currentFocusAppID !== appID &&
- currentFocusAppID !== undefined
- }
- );
+ enabled:
+ shouldQuery &&
+ props.allowAnnotations &&
+ backendSpec?.supports_annotations &&
+ currentFocusAppID !== appID &&
+ currentFocusAppID !== undefined
+ });
- const createAnnotationMutation = useMutation(
- (data: {
+ const createAnnotationMutation = useMutation({
+ mutationFn: (data: {
projectId: string;
annotationData: AnnotationCreate;
appID: string;
@@ -330,16 +324,16 @@ export const AppView = (props: {
data.sequenceID,
data.annotationData
)
- );
+ });
- const updateAnnotationMutation = useMutation(
- (data: { annotationID: number; annotationData: AnnotationUpdate }) =>
+ const updateAnnotationMutation = useMutation({
+ mutationFn: (data: { annotationID: number; annotationData: AnnotationUpdate }) =>
DefaultService.updateAnnotationApiV0ProjectIdAnnotationIdUpdateAnnotationsPut(
projectId,
data.annotationID,
data.annotationData
)
- );
+ });
useEffect(() => {
const steps = data?.steps || [];
diff --git a/telemetry/ui/src/components/routes/app/DataView.tsx b/telemetry/ui/src/components/routes/app/DataView.tsx
index 13aa9993f..bb7533b28 100644
--- a/telemetry/ui/src/components/routes/app/DataView.tsx
+++ b/telemetry/ui/src/components/routes/app/DataView.tsx
@@ -42,8 +42,7 @@ const CommonJsonView = (props: { value: object; collapsed?: number }) => {
value={props.value}
collapsed={collapsed}
enableClipboard={true}
- displayDataTypes={false}
- >
+ displayDataTypes={false}>
{
@@ -53,8 +52,7 @@ const CommonJsonView = (props: { value: object; collapsed?: number }) => {
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
- className="size-4 hover:cursor-pointer"
- >
+ className="size-4 hover:cursor-pointer">
{
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
- className="size-4 hover:cursor-pointer"
- >
+ className="size-4 hover:cursor-pointer">
{
/>
);
- }}
- >
+ }}>
);
};
@@ -201,8 +197,7 @@ export const DataView = (props: { currentStep: Step | undefined; priorStep: Step
checked={viewRawData === 'raw'}
onChange={(checked) => {
setViewRawData(checked ? 'raw' : 'render');
- }}
- >
+ }}>
@@ -435,8 +430,7 @@ export const AttributeView = (props: {
return (
+ className={`${attributeHighlighted && !props.hideHighlight ? 'bg-pink-100' : ''}`}>
{viewRawData === 'render' && (
<>
@@ -529,8 +523,7 @@ export const RenderedField = (props: {
) : typeof value === 'string' ? (
+ className={`${bodyClassNames} whitespace-pre-wrap word-wrap-break-word max-w-[1000px]`}>
{value}
diff --git a/telemetry/ui/src/components/routes/app/ForkSpawnTree.tsx b/telemetry/ui/src/components/routes/app/ForkSpawnTree.tsx
new file mode 100644
index 000000000..fed5b03ea
--- /dev/null
+++ b/telemetry/ui/src/components/routes/app/ForkSpawnTree.tsx
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 { ChildApplicationModel, PointerModel } from '../../../api';
+import { Chip } from '../../common/chip';
+import { Link } from 'react-router-dom';
+
+export const ForkSpawnTree = (props: {
+ appId: string;
+ projectId: string;
+ partitionKey: string | null;
+ parentPointer?: PointerModel;
+ spawningParentPointer?: PointerModel;
+ children: ChildApplicationModel[];
+}) => {
+ if (!props.parentPointer && !props.spawningParentPointer && props.children.length === 0) {
+ return null;
+ }
+
+ const forks = props.children.filter(
+ (c) => c.event_type === ChildApplicationModel.event_type.FORK
+ );
+ const spawns = props.children.filter(
+ (c) => c.event_type === ChildApplicationModel.event_type.SPAWN_START
+ );
+
+ const partitionKeyPath = (pk: string | null) => pk || 'null';
+
+ return (
+
+ {/* Parent links */}
+ {props.parentPointer && (
+
+
+ parent:
+
+ {props.parentPointer.app_id}@{props.parentPointer.sequence_id}
+
+
+ )}
+ {props.spawningParentPointer && (
+
+
+ spawned by:
+
+ {props.spawningParentPointer.app_id}@{props.spawningParentPointer.sequence_id}
+
+
+ )}
+
+ {/* Children */}
+ {forks.length > 0 && (
+
+ forks:
+ {forks.map((child, i) => (
+
+
+ {child.child.app_id}
+
+ ))}
+
+ )}
+ {spawns.length > 0 && (
+
+ spawns:
+ {spawns.map((child, i) => (
+
+
+ {child.child.app_id}
+
+ ))}
+
+ )}
+
+ );
+};
diff --git a/telemetry/ui/src/components/routes/app/GraphView.tsx b/telemetry/ui/src/components/routes/app/GraphView.tsx
index 31a5f37e3..e667c99a3 100644
--- a/telemetry/ui/src/components/routes/app/GraphView.tsx
+++ b/telemetry/ui/src/components/routes/app/GraphView.tsx
@@ -21,7 +21,8 @@ import { ActionModel, ApplicationModel, Step } from '../../../api';
import dagre from 'dagre';
import React, { createContext, useLayoutEffect, useRef, useState } from 'react';
-import ReactFlow, {
+import {
+ ReactFlow,
BaseEdge,
Controls,
EdgeProps,
@@ -30,14 +31,12 @@ import ReactFlow, {
Position,
ReactFlowProvider,
getBezierPath,
- useNodes,
useReactFlow
-} from 'reactflow';
+} from '@xyflow/react';
-import 'reactflow/dist/style.css';
+import '@xyflow/react/dist/style.css';
import { backgroundColorsForIndex } from './AppView';
import { getActionStatus } from '../../../utils';
-import { getSmartEdge } from '@tisoap/react-flow-smart-edge';
const dagreGraph = new dagre.graphlib.Graph();
@@ -119,8 +118,7 @@ const ActionNode = (props: { data: NodeData }) => {
<>
+ className={`${bgColor} ${opacity} ${additionalClasses} text-xl font-sans p-4 rounded-md border`}>
{name}
@@ -149,38 +147,26 @@ export const ActionActionEdge = ({
markerEnd,
data
}: EdgeProps) => {
- const nodes = useNodes();
- data = data as EdgeData;
+ const edgeData = data as EdgeData | undefined;
const { highlightedActions: previousActions, currentAction } =
React.useContext(NodeStateProvider);
const allActionsInPath = [...(previousActions || []), ...(currentAction ? [currentAction] : [])];
const containsFrom = allActionsInPath.some(
- (action) => action.step_start_log.action === data.from
+ (action) => action.step_start_log.action === edgeData?.from
+ );
+ const containsTo = allActionsInPath.some(
+ (action) => action.step_start_log.action === edgeData?.to
);
- const containsTo = allActionsInPath.some((action) => action.step_start_log.action === data.to);
const shouldHighlight = containsFrom && containsTo;
- const getSmartEdgeResponse = getSmartEdge({
- sourcePosition,
- targetPosition,
+
+ const [edgePath] = getBezierPath({
sourceX,
sourceY,
+ sourcePosition,
targetX,
targetY,
- nodes
+ targetPosition
});
- let edgePath = null;
- if (getSmartEdgeResponse !== null) {
- edgePath = getSmartEdgeResponse.svgPathString;
- } else {
- edgePath = getBezierPath({
- sourceX,
- sourceY,
- sourcePosition,
- targetX,
- targetY,
- targetPosition
- })[0];
- }
const style = {
markerColor: shouldHighlight ? 'black' : 'gray',
@@ -188,7 +174,7 @@ export const ActionActionEdge = ({
};
return (
<>
-
+
>
);
};
@@ -348,8 +334,7 @@ export const _Graph = (props: {
highlightedActions: props.previousActions,
hoverAction: props.hoverAction,
currentAction: props.currentAction
- }}
- >
+ }}>