Skip to content

Commit 9d5623b

Browse files
authored
[ui] Add Integrations Marketplace to app (#29027)
## Summary & Motivation Behind a (still-hidden) feature flag, add the Integrations Marketplace to the app. - Retrieve integration data from our new public API. - Populate a top-level marketplace list. - Populate pages for individual integrations. There is still more to add here that will be in followups: - More metadata, namely for pypi/repo information and "built by" data. - The individual integration page needs to be updated to match current mocks. - We have more work to do on the markdown itself, e.g. showing both `pip` and `uv` commands. <img width="1570" alt="Screenshot 2025-04-09 at 12 36 50" src="https://github.com/user-attachments/assets/e3f882a8-313c-489c-b109-b7e8bd6242c5" /> <img width="1411" alt="Screenshot 2025-04-09 at 12 39 57" src="https://github.com/user-attachments/assets/da9adc3c-5406-43b1-8955-b61972587385" /> ## How I Tested These Changes Add myself to the feature flag, view Integrations Marketplace. Verify correct rendering of list page and individual integration pages. Outside of the flag, verify that these routes are not accessible, and that the top nav button link does not appear.
1 parent 7b27d18 commit 9d5623b

File tree

172 files changed

+538
-3078
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

172 files changed

+538
-3078
lines changed

Diff for: js_modules/dagster-ui/packages/ui-components/src/components/Icon.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ import priority_6 from '../icon-svgs/priority_6.svg';
288288
import priority_7 from '../icon-svgs/priority_7.svg';
289289
import priority_8 from '../icon-svgs/priority_8.svg';
290290
import priority_9 from '../icon-svgs/priority_9.svg';
291+
import python from '../icon-svgs/python.svg';
291292
import radio_checked from '../icon-svgs/radio_checked.svg';
292293
import radio_empty from '../icon-svgs/radio_empty.svg';
293294
import rainbow from '../icon-svgs/rainbow.svg';
@@ -697,6 +698,7 @@ export const Icons = {
697698
priority_7,
698699
priority_8,
699700
priority_9,
701+
python,
700702
radio_checked,
701703
radio_empty,
702704
rainbow,
Loading

Diff for: js_modules/dagster-ui/packages/ui-core/package.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
"generate-selection-autocomplete": "ts-node -O '{\"module\": \"commonjs\"}' ./src/scripts/generateSelection.ts && eslint src/selection/generated/ --fix -c .eslintrc.js",
1919
"generate-run-selection": "ts-node -O '{\"module\": \"commonjs\"}' ./src/scripts/generateRunSelection.ts && eslint src/run-selection/generated/ --fix -c .eslintrc.js",
2020
"generate-op-selection": "ts-node -O '{\"module\": \"commonjs\"}' ./src/scripts/generateOpSelection.ts && eslint src/op-selection/generated/ --fix -c .eslintrc.js",
21-
"generate-integration-docs": "ts-node -O '{\"module\": \"commonjs\"}' ./src/scripts/generateIntegrationDocs.ts",
2221
"storybook": "storybook dev -p 6006",
2322
"build-storybook": "storybook build"
2423
},
@@ -48,6 +47,7 @@
4847
"chart.js": "^3.4.1",
4948
"chartjs-adapter-date-fns": "^2.0.0",
5049
"chartjs-plugin-zoom": "^1.1.1",
50+
"clsx": "^2.1.1",
5151
"codemirror": "^5.65.2",
5252
"cronstrue": "^2.51.0",
5353
"dagre": "dagster-io/dagre#0.8.5",
@@ -167,11 +167,9 @@
167167
"resize-observer-polyfill": "^1.5.1",
168168
"storybook": "^8.6.0",
169169
"styled-components": "^5.3.3",
170-
"to-vfile": "^8.0.0",
171170
"ts-node": "10.9.2",
172171
"ts-prune": "0.10.3",
173172
"typescript": "5.5.4",
174-
"vfile-matter": "^5.0.1",
175173
"webpack": "^5.94.0"
176174
},
177175
"babelMacros": {},

Diff for: js_modules/dagster-ui/packages/ui-core/src/app/AppTopNav/AppTopNav.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {NavLink} from 'react-router-dom';
44
import {AppTopNavRightOfLogo} from 'shared/app/AppTopNav/AppTopNavRightOfLogo.oss';
55
import styled from 'styled-components';
66

7+
import {MarketplaceTopNavLink} from '../../integrations/MarketplaceTopNavLink';
8+
import {useFeatureFlags} from '../Flags';
79
import {GhostDaggyWithTooltip} from './GhostDaggy';
810
import {
911
reloadFnForWorkspace,
@@ -20,6 +22,7 @@ interface Props {
2022
}
2123

2224
export const AppTopNav = ({children, allowGlobalReload = false}: Props) => {
25+
const {flagMarketplace} = useFeatureFlags();
2326
const {reloading, tryReload} = useRepositoryLocationReload({
2427
scope: 'workspace',
2528
reloadFn: reloadFnForWorkspace,
@@ -46,6 +49,7 @@ export const AppTopNav = ({children, allowGlobalReload = false}: Props) => {
4649
</ShortcutHandler>
4750
) : null}
4851
<SearchDialog />
52+
{flagMarketplace ? <MarketplaceTopNavLink /> : null}
4953
{children}
5054
</Box>
5155
</AppTopNavContainer>

Diff for: js_modules/dagster-ui/packages/ui-core/src/app/ContentRoot.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const SnapshotRoot = lazy(() => import('../snapshots/SnapshotRoot'));
2424
const GuessJobLocationRoot = lazy(() => import('../workspace/GuessJobLocationRoot'));
2525
const SettingsRoot = lazy(() => import('../settings/SettingsRoot'));
2626
const JobsRoot = lazy(() => import('../jobs/JobsRoot'));
27+
const IntegrationsRoot = lazy(() => import('../integrations/IntegrationsRoot'));
2728

2829
export const ContentRoot = memo(() => {
2930
const {pathname} = useLocation();
@@ -91,6 +92,9 @@ export const ContentRoot = memo(() => {
9192
<Route path="/deployment">
9293
<SettingsRoot />
9394
</Route>
95+
<Route path="/integrations">
96+
<IntegrationsRoot />
97+
</Route>
9498
<Route path="*" isNestingRoute>
9599
<FallthroughRoot />
96100
</Route>

Diff for: js_modules/dagster-ui/packages/ui-core/src/app/FeatureFlags.oss.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export enum FeatureFlag {
66
flagSelectionSyntax = 'flagSelectionSyntax-always-on',
77
flagAssetSelectionWorker = 'flagAssetSelectionWorker',
88
flagUseNewObserveUIs = 'flagUseNewObserveUIs',
9+
flagMarketplace = 'flagMarketplace',
910

1011
// Flags for tests
1112
__TestFlagDefaultNone = '__TestFlagDefaultNone',

Diff for: js_modules/dagster-ui/packages/ui-core/src/integrations/IntegrationIcon.tsx

+6-13
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import {Box, Icon, IconWrapper} from '@dagster-io/ui-components';
22
import styled from 'styled-components';
33

4+
const INTEGRATIONS_ORIGIN_AND_PATH = 'https://integration-registry.dagster.io/logos';
5+
46
interface Props {
57
name: string;
6-
logo: string | {src: string} | null;
8+
logoFilename: string | null;
79
}
810

9-
export const IntegrationIcon = ({name, logo}: Props) => {
11+
export const IntegrationIcon = ({name, logoFilename}: Props) => {
1012
const icon = () => {
11-
if (logo === null) {
13+
if (logoFilename === null) {
1214
return <Icon name="workspace" size={24} />;
1315
}
1416

1517
return (
1618
<IntegrationIconWrapper
1719
role="img"
1820
$size={32}
19-
$img={extractIconSrc(logo)}
21+
$img={`${INTEGRATIONS_ORIGIN_AND_PATH}/${logoFilename}`}
2022
$color={null}
2123
$rotation={null}
2224
aria-label={name}
@@ -36,15 +38,6 @@ export const IntegrationIcon = ({name, logo}: Props) => {
3638
);
3739
};
3840

39-
function extractIconSrc(iconSvg: string | {src: string}) {
40-
// Storybook imports SVGs are string but nextjs imports them as object.
41-
// This is a temporary work around until we can get storybook to import them the same way as nextjs
42-
if (typeof iconSvg !== 'undefined') {
43-
return typeof iconSvg === 'string' ? (iconSvg as any) : iconSvg?.src;
44-
}
45-
return '';
46-
}
47-
4841
const IntegrationIconWrapper = styled(IconWrapper)`
4942
mask-size: contain;
5043
mask-repeat: no-repeat;

Diff for: js_modules/dagster-ui/packages/ui-core/src/integrations/IntegrationPage.tsx

+30-24
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import {Body, Box, Colors, Heading, PageHeader} from '@dagster-io/ui-components';
1+
import {Body, Box, Colors} from '@dagster-io/ui-components';
2+
import clsx from 'clsx';
23
import {useLayoutEffect, useRef, useState} from 'react';
34
import ReactMarkdown from 'react-markdown';
4-
import {CodeComponent} from 'react-markdown/lib/ast-to-react';
5-
import {Link} from 'react-router-dom';
5+
import {Components} from 'react-markdown/lib/ast-to-react';
66
import rehypeHighlight from 'rehype-highlight';
77
import remarkGfm from 'remark-gfm';
88

@@ -17,31 +17,15 @@ interface Props {
1717

1818
export const IntegrationPage = ({integration}: Props) => {
1919
const {
20-
frontmatter: {name, title, excerpt},
21-
logo,
20+
frontmatter: {name, title, excerpt, logoFilename},
2221
content,
2322
} = integration;
2423

2524
return (
2625
<div>
27-
<PageHeader
28-
title={
29-
<Heading>
30-
<Box flex={{direction: 'row', gap: 8}}>
31-
<Link to="/integrations">Integrations Marketplace</Link>
32-
<span> / </span>
33-
<div>{title}</div>
34-
</Box>
35-
</Heading>
36-
}
37-
/>
38-
<Box
39-
padding={{vertical: 24}}
40-
flex={{direction: 'column', gap: 24}}
41-
style={{width: '1100px', margin: '0 auto'}}
42-
>
26+
<Box padding={{vertical: 24}} flex={{direction: 'column', gap: 12}}>
4327
<Box flex={{direction: 'row', gap: 12, alignItems: 'flex-start'}}>
44-
<IntegrationIcon name={name} logo={logo} />
28+
<IntegrationIcon name={name} logoFilename={logoFilename} />
4529
<Box flex={{direction: 'column', gap: 2}} margin={{top: 4}}>
4630
<div style={{fontSize: 18, fontWeight: 600}}>{title}</div>
4731
<Body color={Colors.textLight()}>{excerpt}</Body>
@@ -54,6 +38,7 @@ export const IntegrationPage = ({integration}: Props) => {
5438
rehypePlugins={[[rehypeHighlight, {ignoreMissing: true}]]}
5539
components={{
5640
code: Code,
41+
a: Anchor,
5742
}}
5843
>
5944
{content}
@@ -64,15 +49,36 @@ export const IntegrationPage = ({integration}: Props) => {
6449
);
6550
};
6651

67-
const Code: CodeComponent = (props) => {
68-
const {children, className, ...rest} = props;
52+
const DOCS_ORIGIN = 'https://docs.dagster.io';
53+
54+
const Anchor: Components['a'] = (props) => {
55+
const {children, href, ...rest} = props;
56+
const finalHref = href?.startsWith('/') ? `${DOCS_ORIGIN}${href}` : href;
57+
return (
58+
<a href={finalHref} target="_blank" rel="noreferrer" {...rest}>
59+
{children}
60+
</a>
61+
);
62+
};
63+
64+
const Code: Components['code'] = (props) => {
65+
const {children, className, inline, ...rest} = props;
66+
6967
const codeRef = useRef<HTMLElement>(null);
7068
const [value, setValue] = useState('');
7169

7270
useLayoutEffect(() => {
7371
setValue(codeRef.current?.textContent?.trim() ?? '');
7472
}, [children]);
7573

74+
if (inline) {
75+
return (
76+
<code className={clsx(className, styles.inlineCode)} {...rest}>
77+
{children}
78+
</code>
79+
);
80+
}
81+
7682
return (
7783
<div className={styles.codeBlock}>
7884
<code className={className} {...rest} ref={codeRef}>

Diff for: js_modules/dagster-ui/packages/ui-core/src/integrations/IntegrationTag.tsx

+12-10
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,38 @@ import {IconName} from '@dagster-io/ui-components';
22

33
export enum IntegrationTag {
44
Alerting = 'alerting',
5-
BiTools = 'bi-tools',
6-
ComponentReady = 'component-ready',
5+
BiTools = 'bi',
6+
CommunitySupported = 'community-supported',
77
Compute = 'compute',
8-
EltTools = 'elt-tools',
8+
DagsterSupported = 'dagster-supported',
9+
EtlTools = 'etl',
910
Metadata = 'metadata',
1011
Monitoring = 'monitoring',
11-
Notifications = 'notifications',
1212
Storage = 'storage',
1313
}
1414

15+
export const IntegrationTagKeys: string[] = Object.values(IntegrationTag);
16+
1517
export const IntegrationTagLabel: Record<IntegrationTag, string> = {
1618
[IntegrationTag.Alerting]: 'Alerting',
1719
[IntegrationTag.BiTools]: 'BI tools',
18-
[IntegrationTag.ComponentReady]: 'Component-ready',
20+
[IntegrationTag.CommunitySupported]: 'Community Supported',
1921
[IntegrationTag.Compute]: 'Compute',
20-
[IntegrationTag.EltTools]: 'ELT tools',
22+
[IntegrationTag.DagsterSupported]: 'Dagster Supported',
23+
[IntegrationTag.EtlTools]: 'ETL tools',
2124
[IntegrationTag.Metadata]: 'Metadata',
2225
[IntegrationTag.Monitoring]: 'Monitoring',
23-
[IntegrationTag.Notifications]: 'Notifications',
2426
[IntegrationTag.Storage]: 'Storage',
2527
};
2628

2729
export const IntegrationTagIcon: Record<IntegrationTag, IconName> = {
2830
[IntegrationTag.Alerting]: 'alert',
2931
[IntegrationTag.BiTools]: 'chart_bar',
30-
[IntegrationTag.ComponentReady]: 'repo',
32+
[IntegrationTag.CommunitySupported]: 'people',
3133
[IntegrationTag.Compute]: 'speed',
32-
[IntegrationTag.EltTools]: 'transform',
34+
[IntegrationTag.DagsterSupported]: 'dagster_solid',
35+
[IntegrationTag.EtlTools]: 'transform',
3336
[IntegrationTag.Metadata]: 'metadata',
3437
[IntegrationTag.Monitoring]: 'visibility',
35-
[IntegrationTag.Notifications]: 'notifications',
3638
[IntegrationTag.Storage]: 'download',
3739
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {Redirect, Route, Switch} from 'react-router';
2+
3+
import {MarketplaceRoot} from './MarketplaceRoot';
4+
import {SingleIntegrationRoot} from './SingleIntegrationRoot';
5+
import {useFeatureFlags} from '../app/Flags';
6+
7+
const IntegrationsRoot = () => {
8+
const {flagMarketplace} = useFeatureFlags();
9+
10+
if (!flagMarketplace) {
11+
return <Redirect to="/deployment" />;
12+
}
13+
14+
return (
15+
<Switch>
16+
<Route path="/integrations" component={MarketplaceRoot} exact />
17+
<Route path="/integrations/:integrationName" component={SingleIntegrationRoot} />
18+
</Switch>
19+
);
20+
};
21+
22+
// eslint-disable-next-line import/no-default-export
23+
export default IntegrationsRoot;

0 commit comments

Comments
 (0)