diff --git a/.changeset/fair-buttons-camp.md b/.changeset/fair-buttons-camp.md
new file mode 100644
index 00000000..5acb4e1d
--- /dev/null
+++ b/.changeset/fair-buttons-camp.md
@@ -0,0 +1,5 @@
+---
+'@hyperdx/app': patch
+---
+
+Add User Preferences modal
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b101dbb1..9e1325d5 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,10 +1,10 @@
{
"editor.formatOnSave.eslint": true,
"[typescript]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescriptreact]": {
- "editor.defaultFormatter": "esbenp.prettier-vscode"
+ "editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"cssVariables.lookupFiles": [
"**/*.css",
diff --git a/packages/app/pages/_app.tsx b/packages/app/pages/_app.tsx
index b8118d68..e8507dfb 100644
--- a/packages/app/pages/_app.tsx
+++ b/packages/app/pages/_app.tsx
@@ -18,7 +18,7 @@ import { apiConfigs } from '../src/api';
import * as config from '../src/config';
import { useConfirmModal } from '../src/useConfirm';
import { QueryParamProvider as HDXQueryParamProvider } from '../src/useQueryParam';
-import { UserPreferencesProvider } from '../src/useUserPreferences';
+import { useBackground, useUserPreferences } from '../src/useUserPreferences';
import '@mantine/core/styles.css';
import '@mantine/notifications/styles.css';
@@ -29,12 +29,17 @@ import '../src/LandingPage.scss';
const queryClient = new QueryClient();
import HyperDX from '@hyperdx/browser';
-const mantineTheme: MantineThemeOverride = {
- fontFamily: 'IBM Plex Mono, sans-serif',
+const makeTheme = ({
+ fontFamily,
+}: {
+ fontFamily: string;
+}): MantineThemeOverride => ({
+ fontFamily,
primaryColor: 'green',
primaryShade: 8,
white: '#fff',
fontSizes: {
+ xxs: '11px',
xs: '12px',
sm: '13px',
md: '15px',
@@ -68,13 +73,13 @@ const mantineTheme: MantineThemeOverride = {
],
},
headings: {
- fontFamily: 'IBM Plex Mono, sans-serif',
+ fontFamily,
},
components: {
Modal: {
styles: {
header: {
- fontFamily: 'IBM Plex Mono, sans-serif',
+ fontFamily,
fontWeight: 'bold',
},
},
@@ -108,7 +113,7 @@ const mantineTheme: MantineThemeOverride = {
},
},
},
-};
+});
export type NextPageWithLayout
= NextPage
& {
getLayout?: (page: React.ReactElement) => React.ReactNode;
@@ -119,7 +124,9 @@ type AppPropsWithLayout = AppProps & {
};
export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
+ const { userPreferences } = useUserPreferences();
const confirmModal = useConfirmModal();
+ const background = useBackground(userPreferences);
// port to react query ? (needs to wrap with QueryClientProvider)
useEffect(() => {
@@ -162,6 +169,20 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
});
}, []);
+ useEffect(() => {
+ document.documentElement.className =
+ userPreferences.theme === 'dark' ? 'hdx-theme-dark' : 'hdx-theme-light';
+ // TODO: Remove after migration to Mantine
+ document.body.style.fontFamily = userPreferences.font
+ ? `"${userPreferences.font}", sans-serif`
+ : '"IBM Plex Mono"';
+ }, [userPreferences.theme, userPreferences.font]);
+
+ const mantineTheme = React.useMemo(
+ () => makeTheme({ fontFamily: userPreferences.font }),
+ [userPreferences.font],
+ );
+
const getLayout = Component.getLayout ?? (page => page);
return (
@@ -195,14 +216,13 @@ export default function MyApp({ Component, pageProps }: AppPropsWithLayout) {
-
-
-
- {getLayout()}
-
-
- {confirmModal}
-
+
+
+ {getLayout()}
+
+
+ {confirmModal}
+ {background}
diff --git a/packages/app/src/AppNav.tsx b/packages/app/src/AppNav.tsx
index c0102dd3..747f6361 100644
--- a/packages/app/src/AppNav.tsx
+++ b/packages/app/src/AppNav.tsx
@@ -21,6 +21,7 @@ import {
Loader,
ScrollArea,
} from '@mantine/core';
+import { useDisclosure } from '@mantine/hooks';
import { version } from '../package.json';
@@ -31,6 +32,7 @@ import Icon from './Icon';
import Logo from './Logo';
import { KubernetesFlatIcon } from './SVGIcons';
import type { Dashboard, LogView } from './types';
+import { UserPreferencesModal } from './UserPreferencesModal';
import { useLocalStorage, useWindowSize } from './utils';
import styles from '../styles/AppNav.module.scss';
@@ -902,6 +904,11 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
[dashboards, refetchDashboards, updateDashboard],
);
+ const [
+ UserPreferencesOpen,
+ { close: closeUserPreferences, open: openUserPreferences },
+ ] = useDisclosure(false);
+
return (
<>
@@ -1348,6 +1355,19 @@ export default function AppNav({ fixed = false }: { fixed?: boolean }) {
+
+
+
+ {' '}
+ {!isCollapsed && User Preferences}
+
+
+
)}
+
>
);
}
diff --git a/packages/app/src/ChartPage.tsx b/packages/app/src/ChartPage.tsx
index 9860e6ba..ae7ccf04 100644
--- a/packages/app/src/ChartPage.tsx
+++ b/packages/app/src/ChartPage.tsx
@@ -137,7 +137,6 @@ function GraphPage() {
const { isReady, searchedTimeRange, displayedTimeInputValue, onSearch } =
useNewTimeQuery({
- isUTC: false,
initialDisplayValue: 'Past 1h',
initialTimeRange: defaultTimeRange,
});
diff --git a/packages/app/src/DBQuerySidePanel.tsx b/packages/app/src/DBQuerySidePanel.tsx
index 22e24cdf..b07e1c51 100644
--- a/packages/app/src/DBQuerySidePanel.tsx
+++ b/packages/app/src/DBQuerySidePanel.tsx
@@ -34,7 +34,6 @@ export default function DBQuerySidePanel() {
);
const { searchedTimeRange: dateRange } = useTimeQuery({
- isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,
diff --git a/packages/app/src/DashboardPage.tsx b/packages/app/src/DashboardPage.tsx
index 3bf8c8cc..5e92e24a 100644
--- a/packages/app/src/DashboardPage.tsx
+++ b/packages/app/src/DashboardPage.tsx
@@ -351,8 +351,6 @@ const Tile = forwardRef(
{}}
onPropertySearchClick={() => {}}
onSettled={onSettled}
/>
@@ -692,7 +690,6 @@ export default function DashboardPage() {
const { searchedTimeRange, displayedTimeInputValue, onSearch } =
useNewTimeQuery({
- isUTC: false,
initialDisplayValue: 'Past 1h',
initialTimeRange: defaultTimeRange,
});
diff --git a/packages/app/src/EditChartForm.tsx b/packages/app/src/EditChartForm.tsx
index 8152bb88..ee0217ba 100644
--- a/packages/app/src/EditChartForm.tsx
+++ b/packages/app/src/EditChartForm.tsx
@@ -329,8 +329,6 @@ export const EditSearchChartForm = ({
where: previewConfig.where,
}}
isLive={false}
- isUTC={false}
- setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
@@ -486,8 +484,6 @@ export const EditNumberChartForm = ({
}`,
}}
isLive={false}
- isUTC={false}
- setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
@@ -691,8 +687,6 @@ export const EditTableChartForm = ({
}`,
}}
isLive={false}
- isUTC={false}
- setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
@@ -887,8 +881,6 @@ export const EditHistogramChartForm = ({
}`,
}}
isLive={false}
- isUTC={false}
- setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
@@ -1426,8 +1418,6 @@ export const EditLineChartForm = ({
})}`,
}}
isLive={false}
- isUTC={false}
- setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
diff --git a/packages/app/src/EndpointSidePanel.tsx b/packages/app/src/EndpointSidePanel.tsx
index c8544e91..9434390a 100644
--- a/packages/app/src/EndpointSidePanel.tsx
+++ b/packages/app/src/EndpointSidePanel.tsx
@@ -34,7 +34,6 @@ export default function EndpointSidePanel() {
);
const { searchedTimeRange: dateRange } = useTimeQuery({
- isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,
diff --git a/packages/app/src/HDXMultiSeriesTimeChart.tsx b/packages/app/src/HDXMultiSeriesTimeChart.tsx
index 59d48156..525c5070 100644
--- a/packages/app/src/HDXMultiSeriesTimeChart.tsx
+++ b/packages/app/src/HDXMultiSeriesTimeChart.tsx
@@ -28,7 +28,7 @@ import {
seriesToUrlSearchQueryParam,
} from './ChartUtils';
import type { ChartSeries, NumberFormat } from './types';
-import useUserPreferences, { TimeFormat } from './useUserPreferences';
+import { useUserPreferences } from './useUserPreferences';
import { formatNumber } from './utils';
import { semanticKeyedColor, TIME_TOKENS, truncateMiddle } from './utils';
@@ -38,7 +38,9 @@ const MAX_LEGEND_ITEMS = 4;
const HDXLineChartTooltip = withErrorBoundary(
memo((props: any) => {
- const timeFormat: TimeFormat = useUserPreferences().timeFormat;
+ const {
+ userPreferences: { timeFormat },
+ } = useUserPreferences();
const tsFormat = TIME_TOKENS[timeFormat];
const { active, payload, label, numberFormat } = props;
if (active && payload && payload.length) {
@@ -217,7 +219,9 @@ const MemoChart = memo(function MemoChart({
}, [groupKeys, displayType, lineNames, graphResults]);
const sizeRef = useRef<[number, number]>([0, 0]);
- const timeFormat: TimeFormat = useUserPreferences().timeFormat;
+ const {
+ userPreferences: { timeFormat },
+ } = useUserPreferences();
const tsFormat = TIME_TOKENS[timeFormat];
// Gets the preffered time format from User Preferences, then converts it to a formattable token
diff --git a/packages/app/src/KubernetesDashboardPage.tsx b/packages/app/src/KubernetesDashboardPage.tsx
index 5ce9cc49..a14aba1e 100644
--- a/packages/app/src/KubernetesDashboardPage.tsx
+++ b/packages/app/src/KubernetesDashboardPage.tsx
@@ -753,7 +753,6 @@ export default function KubernetesDashboardPage() {
setDisplayedTimeInputValue,
onSearch,
} = useTimeQuery({
- isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,
@@ -991,8 +990,6 @@ export default function KubernetesDashboardPage() {
'object.regarding.name': 'Name',
}}
isLive={false}
- isUTC={false}
- setIsUTC={() => {}}
onPropertySearchClick={() => {}}
showServiceColumn={false}
/>
diff --git a/packages/app/src/LogTable.tsx b/packages/app/src/LogTable.tsx
index bd4f0242..b2254672 100644
--- a/packages/app/src/LogTable.tsx
+++ b/packages/app/src/LogTable.tsx
@@ -28,8 +28,7 @@ import InstallInstructionsModal from './InstallInstructionsModal';
import LogLevel from './LogLevel';
import { useSearchEventStream } from './search';
import { UNDEFINED_WIDTH } from './tableUtils';
-import type { TimeFormat } from './useUserPreferences';
-import useUserPreferences from './useUserPreferences';
+import { useUserPreferences } from './useUserPreferences';
import { useLocalStorage, usePrevious, useWindowSize } from './utils';
import { TIME_TOKENS } from './utils';
@@ -142,19 +141,16 @@ function LogTableSettingsModal({
onHide,
onDone,
initialAdditionalColumns,
- initialIsUTC,
initialWrapLines,
downloadCSVButton,
}: {
initialAdditionalColumns: string[];
- initialIsUTC: boolean;
initialWrapLines: boolean;
show: boolean;
onHide: () => void;
onDone: (settings: {
additionalColumns: string[];
wrapLines: boolean;
- isUTC: boolean;
}) => void;
downloadCSVButton: JSX.Element;
}) {
@@ -162,7 +158,6 @@ function LogTableSettingsModal({
initialAdditionalColumns,
);
const [wrapLines, setWrapLines] = useState(initialWrapLines);
- const [isUTC, setIsUTC] = useState(initialIsUTC);
return (
setWrapLines(!wrapLines)}
label="Wrap Lines"
/>
- setIsUTC(!isUTC)}
- label="Use UTC time instead of local time"
- />
+
+ UTC setting moved to User Preferences
+
Download Search Results
{downloadCSVButton}
@@ -205,7 +195,7 @@ function LogTableSettingsModal({
variant="outline-success"
className="fs-7 text-muted-hover"
onClick={() => {
- onDone({ additionalColumns, wrapLines, isUTC });
+ onDone({ additionalColumns, wrapLines });
onHide();
}}
>
@@ -225,7 +215,6 @@ export const RawLogTable = memo(
tableId,
displayedColumns,
fetchNextPage,
- formatUTC,
hasNextPage,
highlightedLineId,
isLive,
@@ -261,7 +250,6 @@ export const RawLogTable = memo(
// value: string | number | boolean,
// ) => void;
hasNextPage: boolean;
- formatUTC: boolean;
highlightedLineId: string | undefined;
onScroll: (scrollTop: number) => void;
isLive: boolean;
@@ -283,7 +271,9 @@ export const RawLogTable = memo(
const { width } = useWindowSize();
const isSmallScreen = (width ?? 1000) < 900;
- const timeFormat: TimeFormat = useUserPreferences().timeFormat;
+ const {
+ userPreferences: { timeFormat, isUTC },
+ } = useUserPreferences();
const tsFormat = TIME_TOKENS[timeFormat];
const [columnSizeStorage, setColumnSizeStorage] = useLocalStorage<
@@ -340,13 +330,13 @@ export const RawLogTable = memo(
header: () =>
isSmallScreen
? 'Time'
- : `Timestamp${formatUTC ? ' (UTC)' : ' (Local)'}`,
+ : `Timestamp${isUTC ? ' (UTC)' : ' (Local)'}`,
cell: info => {
// FIXME: since original timestamp doesn't come with timezone info
const date = new Date(info.getValue
());
return (
- {formatUTC
+ {isUTC
? formatInTimeZone(
date,
'Etc/UTC',
@@ -436,7 +426,7 @@ export const RawLogTable = memo(
},
],
[
- formatUTC,
+ isUTC,
highlightedLineId,
onRowExpandClick,
displayedColumns,
@@ -795,10 +785,8 @@ export default function LogTable({
highlightedLineId,
onPropertySearchClick,
onRowExpandClick,
- formatUTC,
isLive,
onScroll,
- setIsUTC,
onEnd,
onShowPatternsClick,
tableId,
@@ -817,10 +805,8 @@ export default function LogTable({
value: string | number | boolean,
) => void;
onRowExpandClick: (logId: string, sortKey: string) => void;
- formatUTC: boolean;
onScroll: (scrollTop: number) => void;
isLive: boolean;
- setIsUTC: (isUTC: boolean) => void;
onEnd?: () => void;
onShowPatternsClick?: () => void;
tableId?: string;
@@ -837,6 +823,10 @@ export default function LogTable({
const resultsKey = [searchedQuery, displayedColumns, isLive].join(':');
+ const {
+ userPreferences: { isUTC },
+ } = useUserPreferences();
+
const {
results: searchResults,
resultsKey: searchResultsKey,
@@ -891,16 +881,14 @@ export default function LogTable({
onHide={() => setInstructionsOpen(false)}
/>
setSettingsOpen(false)}
- onDone={({ additionalColumns, wrapLines, isUTC }) => {
+ onDone={({ additionalColumns, wrapLines }) => {
setDisplayedColumns(additionalColumns);
setWrapLines(wrapLines);
- setIsUTC(isUTC);
}}
downloadCSVButton={
;
showServiceColumn?: boolean;
@@ -34,7 +31,6 @@ export function LogTableWithSidePanel({
property: string,
value: string | number | boolean,
) => void;
- setIsUTC: (isUTC: boolean) => void;
onPropertyAddClick?: (name: string, value: string | boolean | number) => void;
onRowExpandClick?: (logId: string, sortKey: string) => void;
@@ -111,12 +107,10 @@ export function LogTableWithSidePanel({
) : null}
{
setOpenedLog({ id, sortKey });
diff --git a/packages/app/src/NamespaceDetailsSidePanel.tsx b/packages/app/src/NamespaceDetailsSidePanel.tsx
index d84695b6..2f073a30 100644
--- a/packages/app/src/NamespaceDetailsSidePanel.tsx
+++ b/packages/app/src/NamespaceDetailsSidePanel.tsx
@@ -166,8 +166,6 @@ function NamespaceLogs({
where: _where,
}}
isLive={false}
- isUTC={false}
- setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
@@ -192,7 +190,6 @@ export default function NamespaceDetailsSidePanel() {
}, [namespaceName]);
const { searchedTimeRange: dateRange } = useTimeQuery({
- isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,
diff --git a/packages/app/src/NodeDetailsSidePanel.tsx b/packages/app/src/NodeDetailsSidePanel.tsx
index 62f7dd01..7b35a4f4 100644
--- a/packages/app/src/NodeDetailsSidePanel.tsx
+++ b/packages/app/src/NodeDetailsSidePanel.tsx
@@ -181,8 +181,6 @@ function NodeLogs({
where: _where,
}}
isLive={false}
- isUTC={false}
- setIsUTC={() => {}}
onPropertySearchClick={() => {}}
/>
@@ -207,7 +205,6 @@ export default function NodeDetailsSidePanel() {
}, [nodeName]);
const { searchedTimeRange: dateRange } = useTimeQuery({
- isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,
diff --git a/packages/app/src/PatternSidePanel.tsx b/packages/app/src/PatternSidePanel.tsx
index 0a450647..aaa3eb29 100644
--- a/packages/app/src/PatternSidePanel.tsx
+++ b/packages/app/src/PatternSidePanel.tsx
@@ -160,7 +160,6 @@ export default function PatternSidePanel({
isLoading={false}
hasNextPage={false}
wrapLines={false}
- formatUTC={false}
fetchNextPage={useCallback(() => {}, [])}
onScroll={useCallback(() => {}, [])}
/>
diff --git a/packages/app/src/PatternTable.tsx b/packages/app/src/PatternTable.tsx
index 5f0d3f49..03d12f4b 100644
--- a/packages/app/src/PatternTable.tsx
+++ b/packages/app/src/PatternTable.tsx
@@ -132,7 +132,6 @@ const MemoPatternTable = memo(
({
dateRange,
patterns,
- formatUTC,
highlightedPatternId,
isLoading,
onRowExpandClick,
@@ -144,7 +143,6 @@ const MemoPatternTable = memo(
wrapLines: boolean;
isLoading: boolean;
onRowExpandClick: (pattern: Pattern) => void;
- formatUTC: boolean;
highlightedPatternId: string | undefined;
onShowEventsClick?: () => void;
}) => {
@@ -254,7 +252,6 @@ const MemoPatternTable = memo(
},
],
[
- // formatUTC,
highlightedPatternId,
onRowExpandClick,
isSmallScreen,
@@ -437,7 +434,6 @@ const MemoPatternTable = memo(
export default function PatternTable({
config: { where, dateRange },
onRowExpandClick,
- isUTC,
onShowEventsClick,
highlightedPatternId,
}: {
@@ -447,7 +443,6 @@ export default function PatternTable({
};
highlightedPatternId: undefined | string;
onRowExpandClick: (pattern: Pattern) => void;
- isUTC: boolean;
onShowEventsClick?: () => void;
}) {
const { data: histogramResults, isLoading: isHistogramResultsLoading } =
@@ -495,7 +490,6 @@ export default function PatternTable({
highlightedPatternId={highlightedPatternId}
patterns={patterns?.data ?? []}
isLoading={isLoading}
- formatUTC={isUTC}
onRowExpandClick={onRowExpandClick}
onShowEventsClick={onShowEventsClick}
/>
diff --git a/packages/app/src/PatternTableWithSidePanel.tsx b/packages/app/src/PatternTableWithSidePanel.tsx
index f4f519c8..41ebe3db 100644
--- a/packages/app/src/PatternTableWithSidePanel.tsx
+++ b/packages/app/src/PatternTableWithSidePanel.tsx
@@ -7,15 +7,12 @@ import PatternTable from './PatternTable';
function PatternTableWithSidePanel({
config,
- isUTC,
onShowEventsClick,
}: {
config: {
where: string;
dateRange: [Date, Date];
};
- isUTC: boolean;
-
onShowEventsClick?: () => void;
}) {
const [openedPattern, setOpenedPattern] = useState();
@@ -38,7 +35,6 @@ function PatternTableWithSidePanel({
) : null}
{}}
onPropertySearchClick={() => {}}
columnNameMap={{
'k8s.container.name': 'Container',
@@ -179,7 +177,6 @@ export default function PodDetailsSidePanel() {
}, [podName]);
const { searchedTimeRange: dateRange } = useTimeQuery({
- isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,
diff --git a/packages/app/src/SearchPage.tsx b/packages/app/src/SearchPage.tsx
index 346679e0..d0e3fb60 100644
--- a/packages/app/src/SearchPage.tsx
+++ b/packages/app/src/SearchPage.tsx
@@ -48,6 +48,7 @@ import SearchTimeRangePicker from './SearchTimeRangePicker';
import { Tags } from './Tags';
import { useTimeQuery } from './timeQuery';
import { useDisplayedColumns } from './useDisplayedColumns';
+import { useUserPreferences } from './useUserPreferences';
import 'react-modern-drawer/dist/index.css';
import styles from '../styles/SearchPage.module.scss';
@@ -90,7 +91,6 @@ const HDXHistogram = memo(
config: { dateRange, where },
onTimeRangeSelect,
isLive,
- isUTC,
}: {
config: {
dateRange: [Date, Date];
@@ -98,8 +98,11 @@ const HDXHistogram = memo(
};
onTimeRangeSelect: (start: Date, end: Date) => void;
isLive: boolean;
- isUTC: boolean;
}) => {
+ const {
+ userPreferences: { isUTC },
+ } = useUserPreferences();
+
const { data: histogramResults, isLoading: isHistogramResultsLoading } =
api.useLogHistogram(
where,
@@ -216,7 +219,7 @@ const HDXHistogram = memo(
}
tick={{ fontSize: 12, fontFamily: 'IBM Plex Mono, monospace' }}
/>
- } />
+ } />
{highlightStart && highlightEnd ? (
@@ -272,8 +275,6 @@ const LogViewerContainer = memo(function LogViewerContainer({
generateChartUrl,
isLive,
setIsLive,
- isUTC,
- setIsUTC,
onShowPatternsClick,
}: {
config: {
@@ -293,8 +294,6 @@ const LogViewerContainer = memo(function LogViewerContainer({
onPropertyAddClick: (name: string, value: string | boolean | number) => void;
isLive: boolean;
setIsLive: (isLive: boolean) => void;
- isUTC: boolean;
- setIsUTC: (isUTC: boolean) => void;
onShowPatternsClick: () => void;
}) {
const [openedLogQuery, setOpenedLogQuery] = useQueryParams(
@@ -362,7 +361,6 @@ const LogViewerContainer = memo(function LogViewerContainer({
{
// If the user scrolls a bit down, kick out of live mode
@@ -375,7 +373,6 @@ const LogViewerContainer = memo(function LogViewerContainer({
highlightedLineId={openedLog?.id}
config={config}
onPropertySearchClick={onPropertySearchClick}
- formatUTC={isUTC}
onRowExpandClick={useCallback(
(id: string, sortKey: string) => {
setOpenedLog({ id, sortKey });
@@ -399,7 +396,6 @@ function SearchPage() {
'search',
);
- const [isUTC, setIsUTC] = useState(false);
const {
isReady,
isLive,
@@ -409,7 +405,7 @@ function SearchPage() {
onSearch,
setIsLive,
onTimeRangeSelect,
- } = useTimeQuery({ isUTC });
+ } = useTimeQuery({});
const [isFirstLoad, setIsFirstLoad] = useState(true);
useEffect(() => {
@@ -462,6 +458,10 @@ function SearchPage() {
[searchInput],
);
+ const {
+ userPreferences: { isUTC },
+ } = useUserPreferences();
+
const [saveSearchModalMode, setSaveSearchModalMode] = useState<
'update' | 'save' | 'hidden'
>('hidden');
@@ -975,7 +975,6 @@ function SearchPage() {
config={chartsConfig}
onTimeRangeSelect={onTimeRangeSelect}
isLive={isLive}
- isUTC={isUTC}
/>
) : null}
@@ -1008,8 +1007,6 @@ function SearchPage() {
onPropertySearchClick={onPropertySearchClick}
isLive={isLive}
setIsLive={setIsLive}
- isUTC={isUTC}
- setIsUTC={setIsUTC}
onShowPatternsClick={() => {
setIsLive(false);
setResultsMode('patterns');
@@ -1017,7 +1014,6 @@ function SearchPage() {
/>
) : (
diff --git a/packages/app/src/SearchTimeRangePicker.tsx b/packages/app/src/SearchTimeRangePicker.tsx
index 9500eac5..25cb1922 100644
--- a/packages/app/src/SearchTimeRangePicker.tsx
+++ b/packages/app/src/SearchTimeRangePicker.tsx
@@ -7,7 +7,7 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import DatePicker from 'react-datepicker';
import { useHotkeys } from 'react-hotkeys-hook';
-import { TimeFormat } from './useUserPreferences';
+import { useUserPreferences } from './useUserPreferences';
import 'react-datepicker/dist/react-datepicker.css';
@@ -39,14 +39,12 @@ export default function SearchTimeRangePicker({
onSearch,
onSubmit,
showLive = false,
- timeFormat = '12h',
}: {
inputValue: string;
setInputValue: (str: string) => any;
onSearch: (rangeStr: string) => void;
onSubmit?: (rangeStr: string) => void;
showLive?: boolean;
- timeFormat?: TimeFormat;
}) {
const inputRef = useRef(null);
@@ -71,6 +69,10 @@ export default function SearchTimeRangePicker({
}
}, [isDatePickerOpen]);
+ const {
+ userPreferences: { timeFormat },
+ } = useUserPreferences();
+
return (
<>
{}}
onPropertySearchClick={() => {}}
showServiceColumn={false}
/>
@@ -639,7 +636,6 @@ export default function ServiceDashboardPage() {
{}}
onPropertySearchClick={() => {}}
/>
)}
diff --git a/packages/app/src/TeamPage.tsx b/packages/app/src/TeamPage.tsx
index 022adad0..36f3f62a 100644
--- a/packages/app/src/TeamPage.tsx
+++ b/packages/app/src/TeamPage.tsx
@@ -42,7 +42,6 @@ import api from './api';
import { withAppNav } from './layout';
import { WebhookFlatIcon } from './SVGIcons';
import { WebhookService } from './types';
-import useUserPreferences, { TimeFormat } from './useUserPreferences';
import { truncateMiddle } from './utils';
import { isValidJson, isValidUrl } from './utils';
@@ -167,9 +166,6 @@ export default function TeamPage() {
const deleteTeamInvitation = api.useDeleteTeamInvitation();
const saveWebhook = api.useSaveWebhook();
const deleteWebhook = api.useDeleteWebhook();
- const setTimeFormat = useUserPreferences().setTimeFormat;
- const timeFormat = useUserPreferences().timeFormat;
- const handleTimeButtonClick = (val: TimeFormat) => setTimeFormat(val);
const hasAdminAccess = true;
@@ -1112,43 +1108,6 @@ export default function TeamPage() {
isSubmitting={saveTeamInvitation.isLoading}
/>
-
- Time Format
-
-
- Note: Only affects your own view and does not propagate to
- other team members.
-
-
-
- 24h
-
-
- 12h
-
-
-
)}
diff --git a/packages/app/src/UserPreferencesModal.tsx b/packages/app/src/UserPreferencesModal.tsx
new file mode 100644
index 00000000..6f20831f
--- /dev/null
+++ b/packages/app/src/UserPreferencesModal.tsx
@@ -0,0 +1,283 @@
+import * as React from 'react';
+import {
+ Autocomplete,
+ Badge,
+ Button,
+ Divider,
+ Group,
+ Input,
+ Modal,
+ Select,
+ Slider,
+ Stack,
+ Switch,
+ Text,
+} from '@mantine/core';
+
+import { UserPreferences, useUserPreferences } from './useUserPreferences';
+
+const OPTIONS_FONTS = [
+ 'IBM Plex Mono',
+ 'Roboto Mono',
+ 'Inter',
+ { value: 'or use your own font', disabled: true },
+];
+
+const OPTIONS_THEMES = [
+ { label: 'Dark', value: 'dark' },
+ { label: 'Light', value: 'light' },
+];
+
+const OPTIONS_MIX_BLEND_MODE = [
+ 'normal',
+ 'multiply',
+ 'screen',
+ 'overlay',
+ 'darken',
+ 'lighten',
+ 'color-dodge',
+ 'color-burn',
+ 'hard-light',
+ 'soft-light',
+ 'difference',
+ 'exclusion',
+ 'hue',
+ 'saturation',
+ 'color',
+ 'luminosity',
+ 'plus-darker',
+ 'plus-lighter',
+];
+
+const SettingContainer = ({
+ label,
+ description,
+ children,
+}: {
+ label: React.ReactNode;
+ description?: React.ReactNode;
+ children: React.ReactNode;
+}) => {
+ return (
+
+
+ {label}
+ {description && (
+
+ {description}
+
+ )}
+
+ {children}
+
+ );
+};
+
+export const UserPreferencesModal = ({
+ opened,
+ onClose,
+}: {
+ opened: boolean;
+ onClose: () => void;
+}) => {
+ const { userPreferences, setUserPreference } = useUserPreferences();
+
+ return (
+
+ Preferences
+
+ Customize your experience
+
+ >
+ }
+ size="lg"
+ padding="lg"
+ keepMounted={false}
+ opened={opened}
+ onClose={onClose}
+ >
+
+
+
+
+
+
+ setUserPreference({
+ isUTC: e.currentTarget.checked,
+ })
+ }
+ />
+
+
+
+ Appearance
+
+ Experimental
+
+
+ }
+ labelPosition="left"
+ mt="sm"
+ />
+
+
+
+
+ options}
+ onChange={value =>
+ setUserPreference({
+ font: value as UserPreferences['font'],
+ })
+ }
+ data={OPTIONS_FONTS}
+ />
+
+
+
+
+ setUserPreference({
+ backgroundEnabled: !userPreferences.backgroundEnabled,
+ })
+ }
+ checked={userPreferences.backgroundEnabled}
+ />
+
+
+ {userPreferences.backgroundEnabled && (
+ <>
+ Background>} labelPosition="left" />
+
+
+
+
+ }
+ >
+ }
+ onChange={e =>
+ setUserPreference({
+ backgroundUrl: e.currentTarget.value,
+ })
+ }
+ />
+
+
+
+ setUserPreference({
+ backgroundOpacity: value,
+ })
+ }
+ />
+
+
+
+ setUserPreference({
+ backgroundBlur: value,
+ })
+ }
+ />
+
+
+
+ >
+ )}
+
+
+ );
+};
diff --git a/packages/app/src/__test__/timeQuery.test.tsx b/packages/app/src/__test__/timeQuery.test.tsx
index 44bc8b99..d744fdad 100644
--- a/packages/app/src/__test__/timeQuery.test.tsx
+++ b/packages/app/src/__test__/timeQuery.test.tsx
@@ -12,6 +12,7 @@ import {
type UseTimeQueryInputType,
type UseTimeQueryReturnType,
} from '../timeQuery';
+import { useUserPreferences } from '../useUserPreferences';
import { TestRouter } from './fixtures';
@@ -23,7 +24,18 @@ jest.mock('next/router', () => ({
useRouter: jest.fn(),
}));
-function TestWrapper({ children }: { children: React.ReactNode }) {
+function TestWrapper({
+ children,
+ isUTC,
+}: {
+ children: React.ReactNode;
+ isUTC?: boolean;
+}) {
+ const { setUserPreference } = useUserPreferences();
+
+ React.useEffect(() => {
+ setUserPreference({ isUTC });
+ }, [setUserPreference, isUTC]);
return (
{children}
);
@@ -72,7 +84,6 @@ describe('useTimeQuery tests', () => {
render(
@@ -89,9 +100,8 @@ describe('useTimeQuery tests', () => {
const timeQueryRef = React.createRef();
render(
-
+
@@ -111,7 +121,6 @@ describe('useTimeQuery tests', () => {
const { rerender } = render(
@@ -122,7 +131,6 @@ describe('useTimeQuery tests', () => {
rerender(
@@ -151,7 +159,6 @@ describe('useTimeQuery tests', () => {
@@ -165,7 +172,6 @@ describe('useTimeQuery tests', () => {
@@ -189,7 +195,6 @@ describe('useTimeQuery tests', () => {
@@ -200,7 +205,6 @@ describe('useTimeQuery tests', () => {
@@ -228,7 +232,6 @@ describe('useTimeQuery tests', () => {
@@ -243,7 +246,6 @@ describe('useTimeQuery tests', () => {
@@ -269,7 +271,6 @@ describe('useTimeQuery tests', () => {
render(
@@ -289,7 +290,6 @@ describe('useTimeQuery tests', () => {
render(
@@ -313,7 +313,6 @@ describe('useTimeQuery tests', () => {
const result = render(
@@ -324,7 +323,6 @@ describe('useTimeQuery tests', () => {
result.rerender(
@@ -347,7 +345,6 @@ describe('useTimeQuery tests', () => {
render(
(undefined);
@@ -401,8 +404,6 @@ export function useTimeQuery({
}
export type UseTimeQueryInputType = {
- /** Whether the displayed value should be in UTC */
- isUTC: boolean;
/**
* Optional initial value to be set as the `displayedTimeInputValue`.
* If no value is provided it will return a date string for the initial
@@ -423,7 +424,6 @@ export type UseTimeQueryReturnType = {
};
export function useNewTimeQuery({
- isUTC,
initialDisplayValue,
initialTimeRange,
}: UseTimeQueryInputType): UseTimeQueryReturnType {
@@ -431,6 +431,10 @@ export function useNewTimeQuery({
// We need to return true in SSR to prevent mismatch issues
const isReady = typeof window === 'undefined' ? true : router.isReady;
+ const {
+ userPreferences: { isUTC },
+ } = useUserPreferences();
+
const [displayedTimeInputValue, setDisplayedTimeInputValue] =
useState(() => {
return initialDisplayValue ?? dateRangeToString(initialTimeRange, isUTC);
diff --git a/packages/app/src/useUserPreferences.tsx b/packages/app/src/useUserPreferences.tsx
index a1a46107..d196eca6 100644
--- a/packages/app/src/useUserPreferences.tsx
+++ b/packages/app/src/useUserPreferences.tsx
@@ -1,58 +1,67 @@
-import React, { useContext, useEffect, useState } from 'react';
+import React from 'react';
+import produce from 'immer';
+import { useAtom } from 'jotai';
+import { atomWithStorage } from 'jotai/utils';
-import { useLocalStorage } from './utils';
-export type TimeFormat = '12h' | '24h';
-
-export const UserPreferences = React.createContext({
- isUTC: false,
- timeFormat: '24h' as TimeFormat,
- setTimeFormat: (timeFormat: TimeFormat) => {},
- setIsUTC: (isUTC: boolean) => {},
-});
-
-export const UserPreferencesProvider = ({
- children,
-}: {
- children: React.ReactNode;
-}) => {
- const [storedTF, setTF] = useLocalStorage('timeFormat', '24h');
- const setTimeFormat = (timeFormat: TimeFormat) => {
- setState(state => ({ ...state, timeFormat }));
- setTF(timeFormat);
- };
- const initState = {
- isUTC: false,
- timeFormat: '24h' as TimeFormat,
- setTimeFormat,
- setIsUTC: (isUTC: boolean) => setState(state => ({ ...state, isUTC })),
- };
-
- const [state, setState] = useState(initState);
-
- // This only runs once in order to grab and set the initial timeFormat from localStorage
- useEffect(() => {
- if (typeof window === 'undefined') {
- return;
- }
- try {
- let timeFormat = window.localStorage.getItem('timeFormat') as TimeFormat;
- if (timeFormat !== null) timeFormat = JSON.parse(timeFormat);
-
- if (timeFormat !== null) {
- setState(state => ({ ...state, timeFormat }));
- }
- } catch (error) {
- console.log(error);
- }
- }, []);
-
- return (
-
- {children}
-
- );
+export type UserPreferences = {
+ isUTC: boolean;
+ timeFormat: '12h' | '24h';
+ theme: 'light' | 'dark';
+ font: 'IBM Plex Mono' | 'Inter';
+ backgroundEnabled?: boolean;
+ backgroundUrl?: string;
+ backgroundBlur?: number;
+ backgroundOpacity?: number;
+ backgroundBlendMode?: string;
};
-export default function useUserPreferences() {
- return useContext(UserPreferences);
-}
+export const userPreferencesAtom = atomWithStorage(
+ 'hdx-user-preferences',
+ {
+ isUTC: false,
+ timeFormat: '12h',
+ theme: 'dark',
+ font: 'IBM Plex Mono',
+ },
+);
+
+export const useUserPreferences = () => {
+ const [userPreferences, setUserPreferences] = useAtom(userPreferencesAtom);
+
+ const setUserPreference = React.useCallback(
+ (preference: Partial) => {
+ setUserPreferences(
+ produce((draft: UserPreferences) => {
+ return { ...draft, ...preference };
+ }),
+ );
+ },
+ [setUserPreferences],
+ );
+
+ return { userPreferences, setUserPreference };
+};
+
+export const useBackground = (prefs: UserPreferences) => {
+ if (!prefs.backgroundEnabled || !prefs.backgroundUrl) {
+ return null;
+ }
+
+ const blurOffset = -1.5 * (prefs.backgroundBlur || 0) + 'px';
+
+ return (
+
+ );
+};
diff --git a/packages/app/styles/app.scss b/packages/app/styles/app.scss
index 89795f9f..2efc10d6 100644
--- a/packages/app/styles/app.scss
+++ b/packages/app/styles/app.scss
@@ -3,7 +3,8 @@
@import url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css');
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700&display=swap');
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;700&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100..700&display=swap');
.inter {
font-family: Inter, Roboto, 'Helvetica Neue', sans-serif;
@@ -767,6 +768,24 @@ div.react-datepicker {
}
}
-// html {
-// filter: invert(1) hue-rotate(180deg);
-// }
+.hdx-background-image {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-size: cover;
+ background-position: center center;
+ background-repeat: no-repeat;
+ mix-blend-mode: screen;
+ z-index: 9999999;
+ pointer-events: none;
+}
+
+html.hdx-theme-light {
+ filter: invert(1) hue-rotate(180deg) brightness(1.05);
+}
+
+html.hdx-theme-light .hdx-background-image {
+ display: none;
+}
diff --git a/packages/app/styles/variables.scss b/packages/app/styles/variables.scss
index cf69a0b2..52395675 100644
--- a/packages/app/styles/variables.scss
+++ b/packages/app/styles/variables.scss
@@ -18,7 +18,7 @@ $bg-dark-grey: #1f2429;
$bg-grey: #21272e;
$bg-purple: #2e273b;
$bg-purple-active: #4a4eb5;
-$body-bg: #0f1216;
+$body-bg: #0f1215;
$code-color: #4bb74a;
$info: $purple;