Skip to content
Merged
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
12 changes: 6 additions & 6 deletions tavern/internal/www/build/asset-manifest.json

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

2 changes: 1 addition & 1 deletion tavern/internal/www/build/index.html

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

4 changes: 0 additions & 4 deletions tavern/internal/www/build/static/css/main.b74cb248.css

This file was deleted.

1 change: 0 additions & 1 deletion tavern/internal/www/build/static/css/main.b74cb248.css.map

This file was deleted.

4 changes: 4 additions & 0 deletions tavern/internal/www/build/static/css/main.eca4e068.css

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions tavern/internal/www/build/static/css/main.eca4e068.css.map

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

21 changes: 12 additions & 9 deletions tavern/internal/www/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom";
import 'react-virtualized/styles.css';
import { TagContextProvider } from "./context/TagContext";
import { AuthorizationContextProvider } from "./context/AuthorizationContext";
import { PollingProvider } from "./context/PollingContext";
import Tasks from "./pages/tasks/Tasks";
import HostList from "./pages/host-list/HostList";
import HostDetails from "./pages/host-details/HostDetails";
Expand Down Expand Up @@ -75,15 +76,17 @@ export const App = () => {
return (
<ChakraProvider theme={theme}>
<AuthorizationContextProvider>
<TagContextProvider>
<UserPreferencesContextProvider>
<FilterProvider>
<SortsProvider>
<RouterProvider router={router} />
</SortsProvider>
</FilterProvider>
</UserPreferencesContextProvider>
</TagContextProvider>
<PollingProvider>
<TagContextProvider>
<UserPreferencesContextProvider>
<FilterProvider>
<SortsProvider>
<RouterProvider router={router} />
</SortsProvider>
</FilterProvider>
</UserPreferencesContextProvider>
</TagContextProvider>
</PollingProvider>
</AuthorizationContextProvider>
</ChakraProvider>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { classNames } from '../../utils/utils';
import logo from '../../assets/eldrich.png';
import { ArrowLeftOnRectangleIcon } from '@heroicons/react/24/outline';
import { usePageNavigation } from './usePageNavigation';
import { PollingCountdown } from '../../context/PollingContext';

type FullSidebarNavProps = {
currNavItem?: string;
Expand Down Expand Up @@ -71,6 +72,7 @@ const FullSidebarNav = ({ currNavItem, handleSidebarMinimized }: FullSidebarNavP
</li>
</ul>
</nav>
<PollingCountdown variant="full" />
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { classNames } from '../../utils/utils';
import { ArrowRightOnRectangleIcon } from '@heroicons/react/24/outline';
import logo from '../../assets/eldrich.png';
import { usePageNavigation } from './usePageNavigation';
import { PollingCountdown } from '../../context/PollingContext';

type MinimizedSidebarNavProps = {
currNavItem?: string;
Expand Down Expand Up @@ -62,6 +63,7 @@ const MinimizedSidebarNav = ({ currNavItem, handleSidebarMinimized }: MinimizedS
</ul>
</nav>
</div>
<PollingCountdown variant="minimal" />
<div className="my-8">
<a href='/'>
<img
Expand Down
2 changes: 2 additions & 0 deletions tavern/internal/www/src/components/page-wrapper/MobileNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
import { Link } from 'react-router-dom';
import { classNames } from '../../utils/utils';
import { usePageNavigation } from './usePageNavigation';
import { PollingCountdown } from '../../context/PollingContext';

type MobileNavProps = {
sidebarOpen: boolean;
Expand Down Expand Up @@ -112,6 +113,7 @@ const MobileNav = ({ handleSidebarOpen, sidebarOpen, currNavItem }: MobileNavPro
</li>
</ul>
</nav>
<PollingCountdown variant="full" />
</div>
</Dialog.Panel>
</Transition.Child>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type Props = {
}

export const PageWrapper: FunctionComponent<Props> = ({ children, currNavItem }) => {
const [sidebarOpen, setSidebarOpen] = useState(false)
const [sidebarOpen, setSidebarOpen] = useState(false);
const { sidebarMinimized, setSidebarMinimized } = useContext(UserPreferencesContext);

return (
Expand Down
48 changes: 48 additions & 0 deletions tavern/internal/www/src/context/PollingContext/PollingContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useApolloClient } from "@apollo/client";
import { createContext, useContext, useEffect, useState } from "react";

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

export const PollingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const apolloClient = useApolloClient();
const [secondsUntilNextPoll, setSecondsUntilNextPoll] = useState(30);

useEffect(() => {
setSecondsUntilNextPoll(30);

const pollInterval = setInterval(() => {
apolloClient.refetchQueries({
include: "active",
});
setSecondsUntilNextPoll(30);
}, 30000);

const countdownTimer = setInterval(() => {
setSecondsUntilNextPoll((prev) => {
if (prev <= 1) {
return 30; // Safety fallback, shouldn't happen if synced
}
return prev - 1;
});
}, 1000);

return () => {
clearInterval(pollInterval);
clearInterval(countdownTimer);
};
}, [apolloClient]);

return (
<PollingContext.Provider value={{ secondsUntilNextPoll }}>
{children}
</PollingContext.Provider>
);
};

export const usePolling = () => {
const context = useContext(PollingContext);
if (context === undefined) {
throw new Error('usePolling must be used within a PollingContextProvider');
}
return context;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { RotateCw } from "lucide-react";
import { usePolling } from "./PollingContext";

type PollingCountdownProps = {
variant: "full" | "minimal" | "icon-only";
};

export const PollingCountdown = ({ variant }: PollingCountdownProps) => {
const { secondsUntilNextPoll } = usePolling();

if (variant === "minimal") {
return (
<div className="flex flex-col items-center justify-center p-2 text-gray-500">
<RotateCw className="h-3 w-3" />
<span className="text-xs mt-1">{secondsUntilNextPoll}s</span>
</div>
);
}

return (
<div className="flex items-center justify-end gap-x- p-2 gap-2 text-gray-500 text-sm border-t border-gray-800">
<RotateCw className="h-3 w-3 shrink-0" />
<span>Next update:</span><span className="w-7">{secondsUntilNextPoll}s</span>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { renderHook } from '@testing-library/react';
import { PollingProvider, usePolling } from '../PollingContext';

// Mock Apollo Client
const mockRefetchQueries = vi.fn();
vi.mock('@apollo/client', () => ({
useApolloClient: () => ({
refetchQueries: mockRefetchQueries,
}),
}));

describe('PollingContext', () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe('PollingProvider initialization', () => {
it('should provide default seconds until next poll', () => {
const { result } = renderHook(() => usePolling(), {
wrapper: PollingProvider,
});

expect(result.current.secondsUntilNextPoll).toBe(30);
});

it('should throw error when usePolling is used outside PollingProvider', () => {
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => { });

try {
renderHook(() => usePolling());
expect(false).toBe(true);
} catch (error: any) {
expect(error.message).toContain('usePolling must be used within a PollingContextProvider');
}

consoleError.mockRestore();
});
});

describe('Provider setup', () => {
it('should set up intervals on mount', () => {
const setIntervalSpy = vi.spyOn(global, 'setInterval');

renderHook(() => usePolling(), {
wrapper: PollingProvider,
});

// Should set up two intervals: one for polling, one for countdown
expect(setIntervalSpy).toHaveBeenCalledTimes(2);
expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 30000);
expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 1000);

setIntervalSpy.mockRestore();
});

it('should clean up intervals on unmount', () => {
const clearIntervalSpy = vi.spyOn(global, 'clearInterval');

const { unmount } = renderHook(() => usePolling(), {
wrapper: PollingProvider,
});

unmount();

expect(clearIntervalSpy).toHaveBeenCalledTimes(2);

clearIntervalSpy.mockRestore();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import { PollingCountdown } from '../PollingCountdown';
import { PollingProvider } from '../PollingContext';

vi.mock('lucide-react', () => ({
RotateCw: (props: any) => <svg data-testid="rotate-icon" {...props} />,
}));

const mockRefetchQueries = vi.fn();
vi.mock('@apollo/client', () => ({
useApolloClient: () => ({
refetchQueries: mockRefetchQueries,
}),
}));

function TestWrapper({ variant }: { variant: 'full' | 'minimal' }) {
return (
<PollingProvider>
<PollingCountdown variant={variant} />
</PollingProvider>
);
}

describe('PollingCountdown', () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe('Rendering variants', () => {

it('should render minimal variant with icon and countdown', () => {
render(<TestWrapper variant="minimal" />);

const icon = screen.getByTestId('rotate-icon');
expect(icon).toBeInTheDocument();

// Should display countdown seconds
expect(screen.getByText(/\d+s/)).toBeInTheDocument();

// Should not display "Next update:" label
expect(screen.queryByText('Next update:')).not.toBeInTheDocument();
});

it('should render full variant with icon, label, and countdown', () => {
render(<TestWrapper variant="full" />);

const icon = screen.getByTestId('rotate-icon');
expect(icon).toBeInTheDocument();

// Should display "Next update:" label
expect(screen.getByText('Next update:')).toBeInTheDocument();

// Should display countdown seconds
expect(screen.getByText(/\d+s/)).toBeInTheDocument();
});
});

describe('Countdown display', () => {
it('should display countdown value in minimal variant', () => {
render(<TestWrapper variant="minimal" />);

// Default countdown is 30s
expect(screen.getByText('30s')).toBeInTheDocument();
});

it('should display countdown value in full variant', () => {
render(<TestWrapper variant="full" />);

// Default countdown is 30s
expect(screen.getByText('30s')).toBeInTheDocument();
});

});
});
2 changes: 2 additions & 0 deletions tavern/internal/www/src/context/PollingContext/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { PollingProvider, usePolling } from './PollingContext'
export { PollingCountdown } from './PollingCountdown'
9 changes: 1 addition & 8 deletions tavern/internal/www/src/context/TagContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const TagContextProvider = ({ children }: { children: React.ReactNode })
}
}

const { loading: isLoading, error, data, startPolling, stopPolling } = useQuery(GET_TAG_FILTERS, PARAMS);
const { loading: isLoading, error, data } = useQuery(GET_TAG_FILTERS, PARAMS);


const getTags = useCallback((data: TagContextQueryResponse) => {
Expand Down Expand Up @@ -148,13 +148,6 @@ export const TagContextProvider = ({ children }: { children: React.ReactNode })
setTags(tags);
}, []);

useEffect(() => {
startPolling(60000);
return () => {
stopPolling();
}
}, [startPolling, stopPolling])

useEffect(() => {
if (data) {
getTags(data)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ export const useDashboardData = (): UseDashboardDataReturn => {
}],
},
notifyOnNetworkStatusChange: true,
fetchPolicy: "cache-and-network",
pollInterval: 60000,
});

const {
Expand All @@ -29,8 +27,6 @@ export const useDashboardData = (): UseDashboardDataReturn => {
error: hostError
} = useQuery<HostQueryTopLevel>(GET_HOST_QUERY, {
notifyOnNetworkStatusChange: true,
pollInterval: 60000,
fetchPolicy: "cache-and-network",
});

const { loading: questFormatLoading, formattedData } = useQuestData(taskData);
Expand Down
Loading
Loading