Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
529 changes: 529 additions & 0 deletions backend.log

Large diffs are not rendered by default.

Binary file added tavern/internal/www/npm_output.log
Binary file not shown.
22 changes: 22 additions & 0 deletions tavern/internal/www/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions tavern/internal/www/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"react-scripts": "5.0.1",
"react-select": "^5.7.0",
"react-virtualized": "^9.22.4",
"react-virtualized-auto-sizer": "^2.0.2",
"react-window": "^2.2.5",
"recharts": "^2.11.0",
"sort-by": "^0.0.2",
"tailwind-variants": "^0.2.1",
Expand All @@ -50,6 +52,7 @@
"test:coverage": "vitest --coverage",
"eject": "react-scripts eject"
},
"proxy": "http://localhost:8000",
"eslintConfig": {
"extends": "react-app"
},
Expand Down
4 changes: 2 additions & 2 deletions tavern/internal/www/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Tasks from "./pages/tasks/Tasks";
import HostList from "./pages/host-list/HostList";
import HostDetails from "./pages/host-details/HostDetails";
import { Dashboard } from "./pages/dashboard";
import Quests from "./pages/quest-list/Quests";
import JulesQuests from "./pages/quest-list/JulesQuests";
import Shell from "./pages/shell/Shell";
import { UserPreferencesContextProvider } from "./context/UserPreferences";
import { FilterProvider } from "./context/FilterContext";
Expand Down Expand Up @@ -43,7 +43,7 @@ const router = createBrowserRouter([
},
{
path: "/quests",
element: <Quests />,
element: <JulesQuests />,
},
{
path: "/tasks/:questId",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ type FilterContextType = {
resetFilters: () => void
}

const FilterContext = createContext<FilterContextType | undefined>(undefined)
export const FilterContext = createContext<FilterContextType | undefined>(undefined)

export function FilterProvider({ children }: { children: React.ReactNode }) {

Expand Down
2 changes: 1 addition & 1 deletion tavern/internal/www/src/context/FilterContext/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Re-export everything from FilterContext
export { FilterProvider, useFilters } from './FilterContext'
export { FilterProvider, useFilters, FilterContext } from './FilterContext'
export type { Filters } from './FilterContext'

// Re-export everything from FilterControls
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useApolloClient } from "@apollo/client";
import { createContext, useContext, useEffect, useState } from "react";

const PollingContext = createContext<{ secondsUntilNextPoll: number } | undefined>(undefined);
export const PollingContext = createContext<{ secondsUntilNextPoll: number } | undefined>(undefined);

export const PollingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const apolloClient = useApolloClient();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ type SortsContextType = {
resetSorts: () => void
}

const SortsContext = createContext<SortsContextType | undefined>(undefined)
export const SortsContext = createContext<SortsContextType | undefined>(undefined)

export function SortsProvider({ children }: { children: React.ReactNode }) {

Expand Down
2 changes: 1 addition & 1 deletion tavern/internal/www/src/context/SortContext/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { SortsProvider, useSorts } from './SortContext'
export { SortsProvider, useSorts, SortsContext } from './SortContext'
export type { Sorts } from './SortContext'
export { default as SortingControls } from './SortingControls'
176 changes: 176 additions & 0 deletions tavern/internal/www/src/pages/quest-list/JulesQuests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import React, { useCallback, useRef, useState, useEffect, useMemo } from "react";
import { List, AutoSizer } from "react-virtualized";
import { useNavigate } from "react-router-dom";

import { PageWrapper } from "../../components/page-wrapper";
import { PageNavItem } from "../../utils/enums";
import QuestHeader from "./components/QuestHeader";
import { FilterControls, FilterPageType } from "../../context/FilterContext/index";
import { SortingControls } from "../../context/SortContext/index";
import { useJulesQuests } from "./useJulesQuests";
import { QuestRow } from "./components/QuestRow";

// --- Styles ---
const HEADER_CELL_CLASS = "px-4 sm:px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider overflow-hidden text-ellipsis whitespace-nowrap";

// We will use flexbox for columns.
const COL_WIDTHS = {
chevron: "w-[40px] flex-none flex justify-center",
name: "flex-[2_1_200px]",
updated: "w-[120px] flex-none",
finished: "w-[80px] flex-none",
output: "w-[80px] flex-none",
error: "w-[80px] flex-none",
creator: "w-[150px] flex-none",
};

const JulesQuests = () => {
const {
data,
loading,
error,
loadMore,
hasNextPage
} = useJulesQuests();

const navigate = useNavigate();
const quests = useMemo(() => data?.quests?.edges || [], [data]);
const rowCount = quests.length;
const listRef = useRef<any>(null);

// State for expanded rows
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());
// Periodically update current date for "5 mins ago"
const [currentDate, setCurrentDate] = useState(new Date());

useEffect(() => {
const timer = setInterval(() => setCurrentDate(new Date()), 60000);
return () => clearInterval(timer);
}, []);

const toggleRow = useCallback((id: string) => {
setExpandedRows(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, []);

// Force recompute row heights when expandedRows changes
useEffect(() => {
if (listRef.current) {
listRef.current.recomputeRowHeights();
listRef.current.forceUpdateGrid(); // Ensure render
}
}, [expandedRows]);

// Handle Infinite Scroll
const onRowsRendered = ({ stopIndex }: { stopIndex: number }) => {
if (hasNextPage && stopIndex >= rowCount - 5) {
loadMore();
}
};

const getRowHeight = useCallback(({ index }: { index: number }) => {
const quest = quests[index]?.node;
if (quest && expandedRows.has(quest.id)) {
return 300; // Expanded height
}
return 80; // Base height
}, [quests, expandedRows]);

const rowRenderer = useCallback(({ index, key, style }: any) => {
return (
<QuestRow
key={key}
index={index}
style={style}
quests={quests}
navigate={navigate}
currentDate={currentDate}
expandedRows={expandedRows}
toggleRow={toggleRow}
/>
);
}, [quests, navigate, currentDate, expandedRows, toggleRow]);

// Cast to any to avoid TS2786 errors with React 18
const ListAny = List as any;
const AutoSizerAny = AutoSizer as any;

return (
<PageWrapper currNavItem={PageNavItem.quests}>
<div className="flex flex-col h-[calc(100vh-4rem)] gap-4">
<QuestHeader />

{/* Controls Section (Filters/Sorts) */}
<div className="flex flex-row justify-between items-center bg-white p-4 rounded-md shadow-sm border border-gray-200">
<div className="flex-1">
<FilterControls type={FilterPageType.QUEST} />
</div>
<div className="ml-4">
<SortingControls type={PageNavItem.quests} />
</div>
</div>

{/* Table Container */}
<div className="flex-1 flex flex-col bg-white rounded-md shadow overflow-hidden border border-gray-200">
{/* Header */}
<div className="flex flex-row bg-gray-50 border-b border-gray-200 shrink-0 pr-[15px]">
<div className={`${HEADER_CELL_CLASS} ${COL_WIDTHS.chevron}`}></div>
<div className={`${HEADER_CELL_CLASS} ${COL_WIDTHS.name}`}>Quest details</div>
<div className={`${HEADER_CELL_CLASS} ${COL_WIDTHS.updated}`}>Updated</div>
<div className={`${HEADER_CELL_CLASS} ${COL_WIDTHS.finished}`}>Finished</div>
<div className={`${HEADER_CELL_CLASS} ${COL_WIDTHS.output}`}>Output</div>
<div className={`${HEADER_CELL_CLASS} ${COL_WIDTHS.error}`}>Error</div>
<div className={`${HEADER_CELL_CLASS} ${COL_WIDTHS.creator}`}>Creator</div>
</div>

{/* Body */}
<div className="flex-1 min-h-0 h-full relative">
{loading && rowCount === 0 ? (
<div className="flex items-center justify-center h-full text-gray-500">
Loading...
</div>
) : error ? (
<div className="flex items-center justify-center h-full text-red-500">
Error: {error.message}
</div>
) : (
<AutoSizerAny>
{({ height, width }: { height: number, width: number }) => {
if (height === 0 || width === 0) return null;
return (
<ListAny
ref={listRef}
height={height}
width={width}
rowCount={rowCount}
rowHeight={getRowHeight}
rowRenderer={rowRenderer}
onRowsRendered={onRowsRendered}
className="scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent focus:outline-none"
overscanRowCount={5}
/>
);
}}
</AutoSizerAny>
)}
</div>

{/* Footer / Status */}
<div className="bg-gray-50 px-4 py-2 border-t border-gray-200 text-xs text-gray-500 flex justify-between shrink-0">
<div>Total: {data?.quests?.totalCount ?? 0}</div>
<div>{loading ? 'Refreshing...' : 'Updated'}</div>
</div>
</div>
</div>
</PageWrapper>
);
};

export default JulesQuests;
Loading
Loading