diff --git a/js_modules/dagster-ui/packages/app-oss/src/InjectedComponents.tsx b/js_modules/dagster-ui/packages/app-oss/src/InjectedComponents.tsx index a9bd866738be9..63b3b69aaa84b 100644 --- a/js_modules/dagster-ui/packages/app-oss/src/InjectedComponents.tsx +++ b/js_modules/dagster-ui/packages/app-oss/src/InjectedComponents.tsx @@ -1,11 +1,13 @@ import {AppTopNavRightOfLogo} from '@dagster-io/ui-core/app/AppTopNav/AppTopNavRightOfLogo.oss'; import {InjectedComponentContext} from '@dagster-io/ui-core/app/InjectedComponentContext'; +import {UserPreferences} from '@dagster-io/ui-core/app/UserSettingsDialog/UserPreferences.oss'; export const InjectedComponents = ({children}: {children: React.ReactNode}) => { return ( {children} diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/RoundedButton.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/RoundedButton.tsx new file mode 100644 index 0000000000000..1eed3b392631c --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-components/src/components/RoundedButton.tsx @@ -0,0 +1,40 @@ +import styled from 'styled-components'; + +import {Colors} from './Color'; +import {IconWrapper} from './Icon'; + +export const RoundedButton = styled.button` + background-color: ${Colors.navButton()}; + border-radius: 24px; + border: 0; + color: ${Colors.navTextSelected()}; + cursor: pointer; + font-size: 14px; + line-height: 20px; + height: 32px; + text-align: left; + display: block; + padding: 0 8px 0 4px; + user-select: none; + + &:focus, + &:active { + outline: none; + } + + ${IconWrapper} { + background: ${Colors.navText()}; + } + + &:hover { + background-color: ${Colors.navButtonHover()}; + + ${IconWrapper} { + background: ${Colors.navTextSelected()}; + } + } + + ${IconWrapper} { + transition: linear 100ms background; + } +`; diff --git a/js_modules/dagster-ui/packages/ui-components/src/index.ts b/js_modules/dagster-ui/packages/ui-components/src/index.ts index bedef133abff2..3ffbd95df1666 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/index.ts +++ b/js_modules/dagster-ui/packages/ui-components/src/index.ts @@ -29,6 +29,7 @@ export * from './components/Popover'; export * from './components/ProductTour'; export * from './components/Radio'; export * from './components/RefreshableCountdown'; +export * from './components/RoundedButton'; export * from './components/Select'; export * from './components/Slider'; export * from './components/Spinner'; diff --git a/js_modules/dagster-ui/packages/ui-core/src/app/InjectedComponentContext.tsx b/js_modules/dagster-ui/packages/ui-core/src/app/InjectedComponentContext.tsx index 77dae253151f0..cd8e40cc2a7e5 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/app/InjectedComponentContext.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/app/InjectedComponentContext.tsx @@ -2,26 +2,34 @@ import React, {useContext} from 'react'; // import using type so that the actual file doesn't get bundled into Cloud if it's not imported directly by cloud. import type {AppTopNavRightOfLogo} from './AppTopNav/AppTopNavRightOfLogo.oss'; +import type {UserPreferences} from './UserSettingsDialog/UserPreferences.oss'; -type AComponentOrNull = +type ComponentType = keyof React.JSX.IntrinsicElements | React.JSXElementConstructor; +type AComponentFromComponent = AComponentWithProps< + React.ComponentProps +>; + +type AComponentWithProps> = | ((props: Props) => React.ReactNode) - | React.MemoExoticComponent<(props: Props) => React.ReactNode> - | null - | undefined; + | React.MemoExoticComponent<(props: Props) => React.ReactNode>; -export const InjectedComponentContext = React.createContext<{ - AppTopNavRightOfLogo: AComponentOrNull>; - OverviewPageAlerts?: AComponentOrNull>; -}>({ +type InjectedComponentContextType = { + AppTopNavRightOfLogo: AComponentFromComponent | null; + OverviewPageAlerts?: AComponentWithProps | null; + UserPreferences?: AComponentFromComponent | null; +}; +export const InjectedComponentContext = React.createContext({ AppTopNavRightOfLogo: null, OverviewPageAlerts: null, }); -export function componentStub(component: keyof React.ContextType) { - return () => { +export function componentStub( + component: TComponentKey, +): NonNullable { + return (props: any) => { const {[component]: Component} = useContext(InjectedComponentContext); if (Component) { - return ; + return ; } return null; }; diff --git a/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsButton.tsx b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsButton.tsx index 919823aceadde..71680ba2372c8 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsButton.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsButton.tsx @@ -2,7 +2,7 @@ import {Colors, Icon, IconWrapper} from '@dagster-io/ui-components'; import {useState} from 'react'; import styled from 'styled-components'; -import {UserSettingsDialog} from './UserSettingsDialog'; +import {UserSettingsDialog} from './UserSettingsDialog/UserSettingsDialog'; import {getVisibleFeatureFlagRows} from './getVisibleFeatureFlagRows'; const SettingsButton = styled.button` diff --git a/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog/UserPreferences.oss.tsx b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog/UserPreferences.oss.tsx new file mode 100644 index 0000000000000..9d024a40d367d --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog/UserPreferences.oss.tsx @@ -0,0 +1,89 @@ +import { + Box, + Button, + Checkbox, + DAGSTER_THEME_KEY, + DagsterTheme, + Icon, + Subheading, +} from '@dagster-io/ui-components'; +import React from 'react'; + +import {useStateWithStorage} from '../../hooks/useStateWithStorage'; +import {SHORTCUTS_STORAGE_KEY} from '../ShortcutHandler'; +import {HourCycleSelect} from '../time/HourCycleSelect'; +import {ThemeSelect} from '../time/ThemeSelect'; +import {TimezoneSelect} from '../time/TimezoneSelect'; +import {automaticLabel} from '../time/browserTimezone'; + +export const UserPreferences = ({ + onChangeRequiresReload, +}: { + onChangeRequiresReload: (requiresReload: boolean) => void; +}) => { + const [shortcutsEnabled, setShortcutsEnabled] = useStateWithStorage( + SHORTCUTS_STORAGE_KEY, + (value: any) => (typeof value === 'boolean' ? value : true), + ); + + const [theme, setTheme] = useStateWithStorage(DAGSTER_THEME_KEY, (value: any) => { + if (value === DagsterTheme.Light || value === DagsterTheme.Dark) { + return value; + } + return DagsterTheme.System; + }); + + const initialShortcutsEnabled = React.useRef(shortcutsEnabled); + const initialTheme = React.useRef(theme); + + const lastChangeValue = React.useRef(false); + React.useEffect(() => { + const didChange = + initialTheme.current !== theme || initialShortcutsEnabled.current !== shortcutsEnabled; + if (lastChangeValue.current !== didChange) { + onChangeRequiresReload(didChange); + lastChangeValue.current = didChange; + } + }, [shortcutsEnabled, theme, onChangeRequiresReload]); + + const trigger = React.useCallback( + (timezone: string) => ( + + ), + [], + ); + + const toggleKeyboardShortcuts = (e: React.ChangeEvent) => { + const {checked} = e.target; + setShortcutsEnabled(checked); + }; + + return ( + <> + + Preferences + + +
Timezone
+ +
+ +
Hour format
+ +
+ +
Theme
+ +
+ +
Enable keyboard shortcuts
+ +
+ + ); +}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog/UserPreferences.tsx b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog/UserPreferences.tsx new file mode 100644 index 0000000000000..bd26387f93d62 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog/UserPreferences.tsx @@ -0,0 +1,3 @@ +import {componentStub} from '../InjectedComponentContext'; + +export const UserPreferences = componentStub('UserPreferences'); diff --git a/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog.tsx b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog/UserSettingsDialog.tsx similarity index 53% rename from js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog.tsx rename to js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog/UserSettingsDialog.tsx index 87fa95d5abdf6..d1a7e8ff23190 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog/UserSettingsDialog.tsx @@ -5,19 +5,12 @@ import { Dialog, DialogBody, DialogFooter, - Icon, Subheading, } from '@dagster-io/ui-components'; -import {DAGSTER_THEME_KEY, DagsterTheme} from '@dagster-io/ui-components/src/theme/theme'; import * as React from 'react'; -import {FeatureFlagType, getFeatureFlags, setFeatureFlags} from './Flags'; -import {SHORTCUTS_STORAGE_KEY} from './ShortcutHandler'; -import {HourCycleSelect} from './time/HourCycleSelect'; -import {ThemeSelect} from './time/ThemeSelect'; -import {TimezoneSelect} from './time/TimezoneSelect'; -import {automaticLabel} from './time/browserTimezone'; -import {useStateWithStorage} from '../hooks/useStateWithStorage'; +import {UserPreferences} from './UserPreferences'; +import {FeatureFlagType, getFeatureFlags, setFeatureFlags} from '../Flags'; type OnCloseFn = (event: React.SyntheticEvent) => void; type VisibleFlag = {key: string; label?: React.ReactNode; flagType: FeatureFlagType}; @@ -54,21 +47,7 @@ const UserSettingsDialogContent = ({onClose, visibleFlags}: DialogContentProps) const [flags, setFlags] = React.useState(() => getFeatureFlags()); const [reloading, setReloading] = React.useState(false); - const [shortcutsEnabled, setShortcutsEnabled] = useStateWithStorage( - SHORTCUTS_STORAGE_KEY, - (value: any) => (typeof value === 'boolean' ? value : true), - ); - - const [theme, setTheme] = useStateWithStorage(DAGSTER_THEME_KEY, (value: any) => { - if (value === DagsterTheme.Light || value === DagsterTheme.Dark) { - return value; - } - return DagsterTheme.System; - }); - const initialFlagState = React.useRef(JSON.stringify([...getFeatureFlags().sort()])); - const initialShortcutsEnabled = React.useRef(shortcutsEnabled); - const initialTheme = React.useRef(theme); React.useEffect(() => { setFeatureFlags(flags); @@ -78,27 +57,10 @@ const UserSettingsDialogContent = ({onClose, visibleFlags}: DialogContentProps) setFlags(flags.includes(flag) ? flags.filter((f) => f !== flag) : [...flags, flag]); }; - const trigger = React.useCallback( - (timezone: string) => ( - - ), - [], - ); - - const toggleKeyboardShortcuts = (e: React.ChangeEvent) => { - const {checked} = e.target; - setShortcutsEnabled(checked); - }; + const [arePreferencesChanged, setAreaPreferencesChanged] = React.useState(false); const anyChange = - initialFlagState.current !== JSON.stringify([...flags.sort()]) || - initialShortcutsEnabled.current !== shortcutsEnabled || - initialTheme.current !== theme; + initialFlagState.current !== JSON.stringify([...flags.sort()]) || arePreferencesChanged; const handleClose = (event: React.SyntheticEvent) => { if (anyChange) { @@ -113,32 +75,7 @@ const UserSettingsDialogContent = ({onClose, visibleFlags}: DialogContentProps) <> - - Preferences - - -
Timezone
- -
- -
Hour format
- -
- -
Theme
- -
- -
Enable keyboard shortcuts
- -
+
diff --git a/js_modules/dagster-ui/packages/ui-core/src/settings/SettingsLeftPane.tsx b/js_modules/dagster-ui/packages/ui-core/src/settings/SettingsLeftPane.tsx index 4dafd7958ba3f..7d9a7f2afde9b 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/settings/SettingsLeftPane.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/settings/SettingsLeftPane.tsx @@ -2,7 +2,7 @@ import {Box, Icon} from '@dagster-io/ui-components'; import {useState} from 'react'; import {useLocation} from 'react-router-dom'; -import {UserSettingsDialog} from '../app/UserSettingsDialog'; +import {UserSettingsDialog} from '../app/UserSettingsDialog/UserSettingsDialog'; import {getVisibleFeatureFlagRows} from '../app/getVisibleFeatureFlagRows'; import {SideNavItem, SideNavItemConfig} from '../ui/SideNavItem';