Skip to content
Open
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
4 changes: 4 additions & 0 deletions telemetry/ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { StreamingChatbotWithTelemetry } from './examples/StreamingChatbot';
import { AdminView } from './components/routes/AdminView';
import { AnnotationsViewContainer } from './components/routes/app/AnnotationsView';
import { DeepResearcherWithTelemetry } from './examples/DeepResearcher';
import { useTheme } from './hooks/useTheme';

/**
* Basic application. We have an AppContainer -- this has a breadcrumb and a sidebar.
Expand All @@ -48,6 +49,9 @@ import { DeepResearcherWithTelemetry } from './examples/DeepResearcher';
* @returns A rendered application object
*/
const App = () => {
// Initialize theme at the app root so the `dark` class is applied on load
// (respects system preference, falls back to stored manual override).
useTheme();
return (
<QueryClientProvider client={new QueryClient()}>
<Router basename={window.__BURR_BASE_PATH__ || ''}>
Expand Down
47 changes: 47 additions & 0 deletions telemetry/ui/src/components/common/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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 { MoonIcon, SunIcon } from '@heroicons/react/24/outline';
import { classNames } from '../../utils/tailwind';
import { useTheme } from '../../hooks/useTheme';

/**
* A simple sun/moon button that toggles between light and dark mode.
* Replaces the previously broken radio toggle referenced in issue #209.
*/
export const ThemeToggle = (props: { showLabel?: boolean }) => {
const { isDark, toggle } = useTheme();
const Icon = isDark ? SunIcon : MoonIcon;
const label = isDark ? 'Switch to light mode' : 'Switch to dark mode';
return (
<button
type="button"
onClick={toggle}
title={label}
aria-label={label}
className={classNames(
'group flex items-center gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold',
'text-gray-700 hover:bg-gray-50 hover:text-dwdarkblue',
'dark:text-gray-200 dark:hover:bg-gray-800 dark:hover:text-white'
)}>
<Icon className="h-6 w-6 shrink-0 text-gray-400 group-hover:text-dwdarkblue dark:group-hover:text-white" />
{props.showLabel && <span>{isDark ? 'Light mode' : 'Dark mode'}</span>}
</button>
);
};
36 changes: 19 additions & 17 deletions telemetry/ui/src/components/nav/appcontainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { classNames } from '../../utils/tailwind';
import React from 'react';
import { DefaultService } from '../../api';
import { useQuery } from 'react-query';
import { ThemeToggle } from '../common/ThemeToggle';

// Define your GitHub logo SVG as a React component
const GithubLogo = () => (
Expand Down Expand Up @@ -64,8 +65,8 @@ const ToggleOpenButton = (props: { open: boolean; toggleSidebar: () => void }) =
return (
<MinimizeMaximizeIcon
className={classNames(
'text-gray-400',
'h-8 w-8 hover:bg-gray-50 rounded-md hover:cursor-pointer'
'text-gray-400 dark:text-gray-500',
'h-8 w-8 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md hover:cursor-pointer'
)}
aria-hidden="true"
onClick={props.toggleSidebar}
Expand Down Expand Up @@ -181,7 +182,7 @@ export const AppContainer = (props: { children: React.ReactNode }) => {

return (
<>
<div className="h-screen w-screen overflow-x-auto">
<div className="h-screen w-screen overflow-x-auto bg-white dark:bg-gray-900">
<Transition.Root show={smallSidebarOpen} as={Fragment}>
<Dialog as="div" className="relative z-50 lg:hidden" onClose={setSmallSidebarOpen}>
<Transition.Child
Expand Down Expand Up @@ -224,7 +225,7 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
</div>
</Transition.Child>
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-2 py-2">
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-2 py-2 dark:bg-gray-900">
<div className="flex h-16 shrink-0 items-center">
<img
className="h-10 w-auto"
Expand Down Expand Up @@ -280,7 +281,7 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
sidebarOpen ? 'h-screen lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col' : ''
}`}>
{/* Sidebar component, swap this element with another sidebar if you like */}
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 py-2">
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 py-2 dark:border-gray-700 dark:bg-gray-900">
<div className="flex h-16 shrink-0 items-center">
<img
className="h-12 w-auto"
Expand All @@ -299,14 +300,14 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
to={item.href}
className={classNames(
isCurrent(item.href, item.linkType)
? 'bg-gray-50'
: 'hover:bg-gray-50',
'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold text-gray-700'
? 'bg-gray-50 dark:bg-gray-800'
: 'hover:bg-gray-50 dark:hover:bg-gray-800',
'group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold text-gray-700 dark:text-gray-200'
)}
target={item.linkType === 'external' ? '_blank' : undefined}
rel={item.linkType === 'external' ? 'noreferrer' : undefined}>
<item.icon
className="h-6 w-6 shrink-0 text-gray-400"
className="h-6 w-6 shrink-0 text-gray-400 dark:text-gray-500"
aria-hidden="true"
/>
{item.name}
Expand All @@ -318,12 +319,12 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
<Disclosure.Button
className={classNames(
isCurrent(item.href, item.linkType)
? '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'
? 'bg-gray-50 dark:bg-gray-800'
: 'hover:bg-gray-50 dark:hover:bg-gray-800',
'flex items-center w-full text-left rounded-md p-2 gap-x-3 text-sm leading-6 font-semibold text-gray-700 dark:text-gray-200'
)}>
<item.icon
className="h-6 w-6 shrink-0 text-gray-400"
className="h-6 w-6 shrink-0 text-gray-400 dark:text-gray-500"
aria-hidden="true"
/>
{item.name}
Expand All @@ -342,9 +343,9 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
to={subItem.href}
className={classNames(
isCurrent(subItem.href, subItem.linkType)
? 'bg-gray-50'
: 'hover:bg-gray-50',
'block rounded-md py-2 pr-2 pl-9 text-sm leading-6 text-gray-700'
? 'bg-gray-50 dark:bg-gray-800'
: 'hover:bg-gray-50 dark:hover:bg-gray-800',
'block rounded-md py-2 pr-2 pl-9 text-sm leading-6 text-gray-700 dark:text-gray-300'
)}
target={
subItem.linkType === 'external' ? '_blank' : undefined
Expand All @@ -367,7 +368,8 @@ export const AppContainer = (props: { children: React.ReactNode }) => {
</li>
</ul>
</nav>
<div className="flex justify-start -mx-5">
<div className="flex justify-between items-center -mx-5 px-5">
<ThemeToggle />
<ToggleOpenButton open={sidebarOpen} toggleSidebar={toggleSidebar} />
</div>
</div>
Expand Down
6 changes: 4 additions & 2 deletions telemetry/ui/src/components/nav/breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,13 @@ export const BreadCrumb = () => {
<path d="M5.555 17.776l8-16 .894.448-8 16-.894-.448z" />
</svg>
{isNullPK ? (
<span className="ml-4 text-sm font-medium text-gray-200">no primary key</span>
<span className="ml-4 text-sm font-medium text-gray-200 dark:text-gray-600">
no primary key
</span>
) : (
<Link
to={page.href}
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-700"
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
aria-current={page.current ? 'page' : undefined}
>
{decodeURIComponent(page.name)}
Expand Down
4 changes: 2 additions & 2 deletions telemetry/ui/src/components/routes/ProjectList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,10 @@ export const ProjectListTable = (props: { projects: Project[]; includeAnnotation
return (
<TableRow
key={project.id}
className="hover:bg-gray-50 cursor-pointer"
className="hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
onClick={() => navigate(`/project/${project.id}`)}
>
<TableCell className="font-semibold text-gray-700">
<TableCell className="font-semibold text-gray-700 dark:text-gray-200">
<div className="flex flex-row gap-2">
{chipType !== undefined && <Chip label={chipType} chipType={chipType}></Chip>}
{projectName}
Expand Down
101 changes: 101 additions & 0 deletions telemetry/ui/src/hooks/useTheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* 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 { useCallback, useEffect, useState } from 'react';

export type ThemePreference = 'light' | 'dark' | 'system';

const STORAGE_KEY = 'burr-theme';

const getSystemPrefersDark = (): boolean =>
typeof window !== 'undefined' &&
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches;

const getStoredPreference = (): ThemePreference => {
if (typeof window === 'undefined') {
return 'system';
}
const stored = window.localStorage.getItem(STORAGE_KEY);
if (stored === 'light' || stored === 'dark' || stored === 'system') {
return stored;
}
return 'system';
};

/**
* Applies (or removes) the `dark` class on the root <html> element based on the
* resolved theme. Tailwind is configured with darkMode: 'class', so this is what
* actually switches the styling.
*/
const applyDarkClass = (isDark: boolean) => {
const root = document.documentElement;
if (isDark) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
};

/**
* Theme hook that respects system preference by default, allows a manual
* override, and persists the choice to localStorage.
*
* - `preference` is the user's stored choice ('light' | 'dark' | 'system').
* - `isDark` is the resolved value actually applied to the DOM.
* - `setPreference` updates and persists the choice.
* - `toggle` cycles between light and dark (collapsing 'system' to its resolved value first).
*/
export const useTheme = () => {
const [preference, setPreferenceState] = useState<ThemePreference>(getStoredPreference);
const [systemPrefersDark, setSystemPrefersDark] = useState<boolean>(getSystemPrefersDark);

// Keep track of the system preference so 'system' mode reacts to OS changes.
useEffect(() => {
if (!window.matchMedia) {
return;
}
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (event: MediaQueryListEvent) => setSystemPrefersDark(event.matches);
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, []);

const isDark = preference === 'system' ? systemPrefersDark : preference === 'dark';

// Apply the resolved theme to the DOM whenever it changes.
useEffect(() => {
applyDarkClass(isDark);
}, [isDark]);

const setPreference = useCallback((next: ThemePreference) => {
setPreferenceState(next);
if (next === 'system') {
window.localStorage.removeItem(STORAGE_KEY);
} else {
window.localStorage.setItem(STORAGE_KEY, next);
}
}, []);

const toggle = useCallback(() => {
setPreference(isDark ? 'light' : 'dark');
}, [isDark, setPreference]);

return { preference, isDark, setPreference, toggle };
};
2 changes: 1 addition & 1 deletion telemetry/ui/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,5 @@ module.exports = {
plugins: [
require("tailwindcss-question-mark"),
],
darkMode: 'false',
darkMode: 'class',
};
Loading