diff --git a/js_modules/dagster-ui/packages/ui-core/src/app/AppProvider.tsx b/js_modules/dagster-ui/packages/ui-core/src/app/AppProvider.tsx index 623bedec07c49..9c713c78706f5 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/app/AppProvider.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/app/AppProvider.tsx @@ -28,6 +28,7 @@ import {migrateLocalStorageKeys} from './migrateLocalStorageKeys'; import {TimeProvider} from './time/TimeContext'; import {AssetLiveDataProvider} from '../asset-data/AssetLiveDataProvider'; import {AssetRunLogObserver} from '../asset-graph/AssetRunLogObserver'; +import {CodeLinkProtocolProvider} from '../code-links/CodeLinkProtocol'; import {DeploymentStatusProvider, DeploymentStatusType} from '../instance/DeploymentStatusProvider'; import {InstancePageContext} from '../instance/InstancePageContext'; import {PerformancePageNavigationListener} from '../performance'; @@ -139,25 +140,27 @@ export const AppProvider = (props: AppProviderProps) => { - - - - - - - - - {props.children} - - - - - - - - - - + + + + + + + + + + {props.children} + + + + + + + + + + + diff --git a/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog/UserSettingsDialog.tsx b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog/UserSettingsDialog.tsx index cbe92c09d4ea2..2a0ddbef0f5c2 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog/UserSettingsDialog.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/app/UserSettingsDialog/UserSettingsDialog.tsx @@ -2,14 +2,18 @@ import { Box, Button, Checkbox, + Colors, Dialog, DialogBody, DialogFooter, + Icon, Subheading, + Tooltip, } from '@dagster-io/ui-components'; import * as React from 'react'; import {UserPreferences} from './UserPreferences'; +import {CodeLinkProtocolSelect} from '../../code-links/CodeLinkProtocol'; import {FeatureFlagType, getFeatureFlags, setFeatureFlags} from '../Flags'; type OnCloseFn = (event: React.SyntheticEvent) => void; @@ -71,6 +75,52 @@ const UserSettingsDialogContent = ({onClose, visibleFlags}: DialogContentProps) } }; + const experimentalSettings = visibleFlags.map(({key, label, flagType}) => ( + +
{label || key}
+ toggleFlag(flagType)} + /> +
+ )); + + experimentalSettings.push( + + + Editor link protocol + + URL protocol to use when linking to definitions in code +

+ {'{'}FILE{'}'} and {'{'}LINE{'}'} replaced by filepath and line +
+ number, respectively + + } + > + +
+
+ +
, + ); + return ( <> @@ -81,20 +131,7 @@ const UserSettingsDialogContent = ({onClose, visibleFlags}: DialogContentProps) Experimental features - {visibleFlags.map(({key, label, flagType}) => ( - -
{label || key}
- toggleFlag(flagType)} - /> -
- ))} + {experimentalSettings}
diff --git a/js_modules/dagster-ui/packages/ui-core/src/code-links/CodeLink.tsx b/js_modules/dagster-ui/packages/ui-core/src/code-links/CodeLink.tsx new file mode 100644 index 0000000000000..0b8aff6087586 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/code-links/CodeLink.tsx @@ -0,0 +1,18 @@ +import {ExternalAnchorButton} from '@dagster-io/ui-components/src/components/Button'; +import {Icon} from '@dagster-io/ui-components/src/components/Icon'; +import * as React from 'react'; + +import {CodeLinkProtocolContext} from './CodeLinkProtocol'; + +export const CodeLink = ({file, lineNumber}: {file: string; lineNumber: number}) => { + const [codeLinkProtocol, _] = React.useContext(CodeLinkProtocolContext); + + const codeLink = codeLinkProtocol.protocol + .replace('{FILE}', file) + .replace('{LINE}', lineNumber.toString()); + return ( + } href={codeLink}> + Open in editor + + ); +}; diff --git a/js_modules/dagster-ui/packages/ui-core/src/code-links/CodeLinkProtocol.tsx b/js_modules/dagster-ui/packages/ui-core/src/code-links/CodeLinkProtocol.tsx new file mode 100644 index 0000000000000..f9a815508b82c --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/code-links/CodeLinkProtocol.tsx @@ -0,0 +1,106 @@ +import { + Box, + Button, + CaptionMono, + Icon, + Menu, + MenuItem, + Select, + TextInput, +} from '@dagster-io/ui-components'; +import * as React from 'react'; + +import {useStateWithStorage} from '../hooks/useStateWithStorage'; + +export const CodeLinkProtocolKey = 'CodeLinkProtocolPreference'; + +const POPULAR_PROTOCOLS: {[name: string]: string} = { + 'vscode://file/{FILE}:{LINE}': 'Visual Studio Code', + '': 'Custom', +}; + +const DEFAULT_PROTOCOL = {protocol: Object.keys(POPULAR_PROTOCOLS)[0]!, custom: false}; + +type ProtocolData = { + protocol: string; + custom: boolean; +}; + +export const CodeLinkProtocolContext = React.createContext< + [ProtocolData, React.Dispatch>] +>([DEFAULT_PROTOCOL, () => '']); + +export const CodeLinkProtocolProvider = ({children}: {children: React.ReactNode}) => { + const state = useStateWithStorage( + CodeLinkProtocolKey, + (x) => x ?? DEFAULT_PROTOCOL, + ); + + return ( + {children} + ); +}; + +export const CodeLinkProtocolSelect = ({}) => { + const [codeLinkProtocol, setCodeLinkProtocol] = React.useContext(CodeLinkProtocolContext); + const isCustom = codeLinkProtocol.custom; + + return ( + + + popoverProps={{ + position: 'bottom-left', + modifiers: {offset: {enabled: true, offset: '-12px, 8px'}}, + }} + activeItem={isCustom ? '' : codeLinkProtocol.protocol} + inputProps={{style: {width: '300px'}}} + items={Object.keys(POPULAR_PROTOCOLS)} + itemPredicate={(query: string, protocol: string) => + protocol.toLowerCase().includes(query.toLowerCase()) + } + itemRenderer={(protocol: string, props: any) => ( + + )} + itemListRenderer={({renderItem, filteredItems}) => { + const renderedItems = filteredItems.map(renderItem).filter(Boolean); + return {renderedItems}; + }} + noResults={} + onItemSelect={(protocol: string) => + setCodeLinkProtocol({protocol, custom: protocol === ''}) + } + > + + + {isCustom ? ( + + setCodeLinkProtocol({ + protocol: e.target.value, + custom: true, + }) + } + placeholder="protocol://{FILE}:{LINE}" + /> + ) : ( + + {codeLinkProtocol.protocol} + + )} + + ); +};