@nospt/plugin-dev-ai-hub 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/DevAiHubResourceClient.esm.js +97 -0
- package/dist/api/DevAiHubResourceClient.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/AiResourcesPage.esm.js +150 -0
- package/dist/components/AiResourcesPage/AiResourcesPage.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/AiResourcesPage.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/AiResourcesPage.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceCard.esm.js +158 -0
- package/dist/components/AiResourcesPage/ResourceCard.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceCard.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/ResourceCard.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js +234 -0
- package/dist/components/AiResourcesPage/ResourceDetailPanel.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceDetailPanel.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/ResourceDetailPanel.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceFilters.esm.js +242 -0
- package/dist/components/AiResourcesPage/ResourceFilters.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceFilters.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/ResourceFilters.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js +366 -0
- package/dist/components/AiResourcesPage/ResourceInstallDialog.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/ResourceInstallDialog.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/ResourceInstallDialog.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/StatTiles.esm.js +49 -0
- package/dist/components/AiResourcesPage/StatTiles.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/StatTiles.module.css.esm.js +8 -0
- package/dist/components/AiResourcesPage/StatTiles.module.css.esm.js.map +1 -0
- package/dist/components/AiResourcesPage/index.esm.js +6 -0
- package/dist/components/AiResourcesPage/typeMeta.esm.js +35 -0
- package/dist/components/AiResourcesPage/typeMeta.esm.js.map +1 -0
- package/dist/components/ToolIcon/ToolIcon.esm.js +26 -9
- package/dist/components/ToolIcon/ToolIcon.esm.js.map +1 -1
- package/dist/hooks/useResourceBody.esm.js +39 -0
- package/dist/hooks/useResourceBody.esm.js.map +1 -0
- package/dist/hooks/useResources.esm.js +26 -0
- package/dist/hooks/useResources.esm.js.map +1 -0
- package/dist/hooks/useTelemetryCounts.esm.js +24 -0
- package/dist/hooks/useTelemetryCounts.esm.js.map +1 -0
- package/dist/index.d.ts +1 -38
- package/dist/index.esm.js +1 -3
- package/dist/index.esm.js.map +1 -1
- package/dist/plugin.esm.js +17 -26
- package/dist/plugin.esm.js.map +1 -1
- package/dist/routes.esm.js +1 -2
- package/dist/routes.esm.js.map +1 -1
- package/package.json +17 -10
- package/.eslintrc.js +0 -1
- package/dev/index.tsx +0 -32
- package/dist/api/DevAiHubClient.esm.js +0 -78
- package/dist/api/DevAiHubClient.esm.js.map +0 -1
- package/dist/components/AssetCard/AssetCard.esm.js +0 -120
- package/dist/components/AssetCard/AssetCard.esm.js.map +0 -1
- package/dist/components/AssetCard/AssetCard.module.css.esm.js +0 -8
- package/dist/components/AssetCard/AssetCard.module.css.esm.js.map +0 -1
- package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js +0 -218
- package/dist/components/AssetDetailPanel/AssetDetailPanel.esm.js.map +0 -1
- package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js +0 -8
- package/dist/components/AssetDetailPanel/AssetDetailPanel.module.css.esm.js.map +0 -1
- package/dist/components/AssetFilters/AssetFilters.esm.js +0 -176
- package/dist/components/AssetFilters/AssetFilters.esm.js.map +0 -1
- package/dist/components/AssetFilters/AssetFilters.module.css.esm.js +0 -8
- package/dist/components/AssetFilters/AssetFilters.module.css.esm.js.map +0 -1
- package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js +0 -138
- package/dist/components/AssetInstallDialog/AssetInstallDialog.esm.js.map +0 -1
- package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js +0 -8
- package/dist/components/AssetInstallDialog/AssetInstallDialog.module.css.esm.js.map +0 -1
- package/dist/components/DevAiHubPage/DevAiHubPage.esm.js +0 -195
- package/dist/components/DevAiHubPage/DevAiHubPage.esm.js.map +0 -1
- package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js +0 -8
- package/dist/components/DevAiHubPage/DevAiHubPage.module.css.esm.js.map +0 -1
- package/dist/components/DevAiHubPage/index.esm.js +0 -6
- package/dist/components/McpConfigPage/McpConfigPage.esm.js +0 -200
- package/dist/components/McpConfigPage/McpConfigPage.esm.js.map +0 -1
- package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js +0 -8
- package/dist/components/McpConfigPage/McpConfigPage.module.css.esm.js.map +0 -1
- package/dist/components/McpConfigPage/index.esm.js +0 -6
- package/dist/components/McpConfigPage/index.esm.js.map +0 -1
- package/dist/hooks/index.esm.js +0 -114
- package/dist/hooks/index.esm.js.map +0 -1
- package/dist/pluginLegacy.esm.js +0 -27
- package/dist/pluginLegacy.esm.js.map +0 -1
- package/package.json-prepack +0 -85
- package/src/api/DevAiHubClient.ts +0 -110
- package/src/components/AssetCard/AssetCard.module.css +0 -153
- package/src/components/AssetCard/AssetCard.test.tsx +0 -182
- package/src/components/AssetCard/AssetCard.tsx +0 -152
- package/src/components/AssetCard/index.ts +0 -1
- package/src/components/AssetDetailPanel/AssetDetailPanel.module.css +0 -207
- package/src/components/AssetDetailPanel/AssetDetailPanel.tsx +0 -313
- package/src/components/AssetDetailPanel/index.ts +0 -1
- package/src/components/AssetFilters/AssetFilters.module.css +0 -67
- package/src/components/AssetFilters/AssetFilters.tsx +0 -219
- package/src/components/AssetFilters/index.ts +0 -2
- package/src/components/AssetInstallDialog/AssetInstallDialog.module.css +0 -72
- package/src/components/AssetInstallDialog/AssetInstallDialog.tsx +0 -176
- package/src/components/AssetInstallDialog/index.ts +0 -1
- package/src/components/DevAiHubPage/DevAiHubPage.module.css +0 -105
- package/src/components/DevAiHubPage/DevAiHubPage.tsx +0 -197
- package/src/components/DevAiHubPage/index.ts +0 -1
- package/src/components/McpConfigDialog/McpConfigDialog.module.css +0 -111
- package/src/components/McpConfigDialog/McpConfigDialog.tsx +0 -271
- package/src/components/McpConfigDialog/index.ts +0 -1
- package/src/components/McpConfigPage/McpConfigPage.module.css +0 -121
- package/src/components/McpConfigPage/McpConfigPage.tsx +0 -242
- package/src/components/McpConfigPage/index.ts +0 -1
- package/src/components/ToolIcon/ToolIcon.tsx +0 -65
- package/src/components/ToolIcon/index.ts +0 -1
- package/src/css-modules.d.ts +0 -4
- package/src/hooks/index.ts +0 -152
- package/src/index.ts +0 -10
- package/src/plugin.tsx +0 -60
- package/src/pluginLegacy.ts +0 -42
- package/src/routes.ts +0 -4
- package/tests/e2e/.auth.json +0 -14
- package/tests/e2e/asset-detail.spec.ts +0 -90
- package/tests/e2e/asset-filtering.spec.ts +0 -145
- package/tests/e2e/asset-installation.spec.ts +0 -74
- package/tests/e2e/dev-ai-hub-page.spec.ts +0 -89
- package/tests/e2e/fixtures/base.ts +0 -61
- package/tests/e2e/fixtures/mock-api.ts +0 -221
- package/tests/e2e/mcp-config.spec.ts +0 -88
- package/tsconfig.json +0 -10
- /package/dist/components/{DevAiHubPage → AiResourcesPage}/index.esm.js.map +0 -0
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { useState, useMemo, type ElementType } from 'react';
|
|
2
|
-
import { useSearchParams } from 'react-router-dom';
|
|
3
|
-
import { Box, Flex, Text, Skeleton, TablePagination } from '@backstage/ui';
|
|
4
|
-
import { RiArticleLine, RiRobot2Line, RiToolsLine, RiGitBranchLine } from '@remixicon/react';
|
|
5
|
-
import type { AssetType, AiTool } from '@nospt/plugin-dev-ai-hub-common';
|
|
6
|
-
import { AssetCard } from '../AssetCard';
|
|
7
|
-
import { AssetFilters } from '../AssetFilters';
|
|
8
|
-
import type { AssetFiltersValue } from '../AssetFilters';
|
|
9
|
-
import { AssetDetailPanel } from '../AssetDetailPanel';
|
|
10
|
-
import { AssetInstallDialog } from '../AssetInstallDialog';
|
|
11
|
-
import { useAssets, useStats, useProviders } from '../../hooks';
|
|
12
|
-
import styles from './DevAiHubPage.module.css';
|
|
13
|
-
|
|
14
|
-
const DEFAULT_FILTERS: AssetFiltersValue = {
|
|
15
|
-
types: [],
|
|
16
|
-
tools: [],
|
|
17
|
-
search: '',
|
|
18
|
-
tags: [],
|
|
19
|
-
providerId: undefined,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const PAGE_SIZE = 24;
|
|
23
|
-
|
|
24
|
-
const STATS_CONFIG: { key: AssetType; label: string; Icon: ElementType; gradient: string; shadow: string }[] = [
|
|
25
|
-
{ key: 'instruction', label: 'Instructions', Icon: RiArticleLine,
|
|
26
|
-
gradient: 'linear-gradient(135deg, #54A0FF 0%, #2980FF 100%)', shadow: '#54A0FF40' },
|
|
27
|
-
{ key: 'agent', label: 'Agents', Icon: RiRobot2Line,
|
|
28
|
-
gradient: 'linear-gradient(135deg, #FF6B9D 0%, #E0507A 100%)', shadow: '#FF6B9D40' },
|
|
29
|
-
{ key: 'skill', label: 'Skills', Icon: RiToolsLine,
|
|
30
|
-
gradient: 'linear-gradient(135deg, #6AB04C 0%, #4A8F2E 100%)', shadow: '#6AB04C40' },
|
|
31
|
-
{ key: 'workflow', label: 'Workflows', Icon: RiGitBranchLine,
|
|
32
|
-
gradient: 'linear-gradient(135deg, #F9CA24 0%, #D4A800 100%)', shadow: '#F9CA2440' },
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
export function DevAiHubPage() {
|
|
36
|
-
const [filters, setFilters] = useState<AssetFiltersValue>(DEFAULT_FILTERS);
|
|
37
|
-
const [page, setPage] = useState(1);
|
|
38
|
-
|
|
39
|
-
const [searchParams, setSearchParams] = useSearchParams();
|
|
40
|
-
const selectedAssetId = searchParams.get('assetId');
|
|
41
|
-
const installAssetId = searchParams.get('installId');
|
|
42
|
-
|
|
43
|
-
const handleViewAsset = (id: string) =>
|
|
44
|
-
setSearchParams(p => { const n = new URLSearchParams(p); n.set('assetId', id); return n; });
|
|
45
|
-
|
|
46
|
-
const handleCloseDetail = () =>
|
|
47
|
-
setSearchParams(p => { const n = new URLSearchParams(p); n.delete('assetId'); return n; });
|
|
48
|
-
|
|
49
|
-
const handleInstallAsset = (id: string) =>
|
|
50
|
-
setSearchParams(p => { const n = new URLSearchParams(p); n.set('installId', id); return n; });
|
|
51
|
-
|
|
52
|
-
const handleCloseInstall = () =>
|
|
53
|
-
setSearchParams(p => { const n = new URLSearchParams(p); n.delete('installId'); return n; });
|
|
54
|
-
|
|
55
|
-
const { stats } = useStats();
|
|
56
|
-
const { providers } = useProviders();
|
|
57
|
-
|
|
58
|
-
const apiFilter = useMemo(
|
|
59
|
-
() => ({
|
|
60
|
-
type: filters.types.length === 1 ? (filters.types[0] as AssetType) : undefined,
|
|
61
|
-
tool: filters.tools.length === 1 ? (filters.tools[0] as AiTool) : undefined,
|
|
62
|
-
search: filters.search || undefined,
|
|
63
|
-
tags: filters.tags.length > 0 ? filters.tags : undefined,
|
|
64
|
-
providerId: filters.providerId || undefined,
|
|
65
|
-
page,
|
|
66
|
-
pageSize: PAGE_SIZE,
|
|
67
|
-
}),
|
|
68
|
-
[filters, page],
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
const { result, loading } = useAssets(apiFilter);
|
|
72
|
-
|
|
73
|
-
const handleFiltersChange = (next: AssetFiltersValue) => {
|
|
74
|
-
setFilters(next);
|
|
75
|
-
setPage(1);
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const totalPages = result ? Math.ceil(result.totalCount / PAGE_SIZE) : 0;
|
|
79
|
-
|
|
80
|
-
const availableTags = useMemo(() => {
|
|
81
|
-
if (!result) return [];
|
|
82
|
-
const tagSet = new Set<string>();
|
|
83
|
-
result.items.forEach(a => a.tags.forEach(t => tagSet.add(t)));
|
|
84
|
-
return Array.from(tagSet).sort();
|
|
85
|
-
}, [result]);
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div className={styles.pageRoot}>
|
|
89
|
-
<div className={styles.content}>
|
|
90
|
-
{/* Stats row */}
|
|
91
|
-
<div className={styles.statsGrid}>
|
|
92
|
-
{STATS_CONFIG.map(({ key, label, Icon, gradient, shadow }) => {
|
|
93
|
-
const isActive = filters.types[0] === key;
|
|
94
|
-
return (
|
|
95
|
-
<div
|
|
96
|
-
key={key}
|
|
97
|
-
className={styles.statCard}
|
|
98
|
-
onClick={() => handleFiltersChange({ ...filters, types: isActive ? [] : [key] })}
|
|
99
|
-
style={{
|
|
100
|
-
background: gradient,
|
|
101
|
-
boxShadow: isActive ? `0 8px 24px ${shadow}` : `0 2px 8px ${shadow}`,
|
|
102
|
-
transform: isActive ? 'translateY(-2px)' : 'none',
|
|
103
|
-
outline: isActive ? '2px solid rgba(255,255,255,0.6)' : 'none',
|
|
104
|
-
outlineOffset: 2,
|
|
105
|
-
}}
|
|
106
|
-
role="button"
|
|
107
|
-
tabIndex={0}
|
|
108
|
-
onKeyDown={e => { if (e.key === 'Enter') handleFiltersChange({ ...filters, types: isActive ? [] : [key] }); }}
|
|
109
|
-
>
|
|
110
|
-
<div className={styles.statCardInner}>
|
|
111
|
-
<Box>
|
|
112
|
-
<Text variant="title-large" weight="bold" className={styles.statValue}>
|
|
113
|
-
{stats ? (stats.byType[key] ?? 0) : <Skeleton style={{ width: 32 }} />}
|
|
114
|
-
</Text>
|
|
115
|
-
<Text variant="body-small" className={styles.statLabel}>
|
|
116
|
-
{label}
|
|
117
|
-
</Text>
|
|
118
|
-
</Box>
|
|
119
|
-
<div className={styles.statIconBox}>
|
|
120
|
-
<Icon size={22} style={{ color: '#fff' }} />
|
|
121
|
-
</div>
|
|
122
|
-
</div>
|
|
123
|
-
</div>
|
|
124
|
-
);
|
|
125
|
-
})}
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
{/* Filters */}
|
|
129
|
-
<AssetFilters
|
|
130
|
-
value={filters}
|
|
131
|
-
onChange={handleFiltersChange}
|
|
132
|
-
availableTags={availableTags}
|
|
133
|
-
providers={providers.length > 1 ? providers : undefined}
|
|
134
|
-
/>
|
|
135
|
-
|
|
136
|
-
{/* Results summary */}
|
|
137
|
-
{result && !loading && (
|
|
138
|
-
<Text variant="body-x-small" color="secondary" style={{ marginBottom: 'var(--bui-space-4)', display: 'block' }}>
|
|
139
|
-
{result.totalCount} asset{result.totalCount !== 1 ? 's' : ''} found
|
|
140
|
-
</Text>
|
|
141
|
-
)}
|
|
142
|
-
|
|
143
|
-
{/* Asset grid — 4 columns on large screens */}
|
|
144
|
-
<div className={styles.assetGrid}>
|
|
145
|
-
{loading
|
|
146
|
-
? Array.from({ length: 8 }).map((_, i) => (
|
|
147
|
-
<Skeleton key={i} style={{ height: 150, borderRadius: 'var(--bui-radius-2)' }} />
|
|
148
|
-
))
|
|
149
|
-
: result?.items.map(asset => (
|
|
150
|
-
<AssetCard
|
|
151
|
-
key={asset.id}
|
|
152
|
-
asset={asset}
|
|
153
|
-
onView={handleViewAsset}
|
|
154
|
-
onInstall={handleInstallAsset}
|
|
155
|
-
/>
|
|
156
|
-
))}
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
{/* Empty state */}
|
|
160
|
-
{!loading && result?.items.length === 0 && (
|
|
161
|
-
<div className={styles.emptyState}>
|
|
162
|
-
<div className={styles.emptyEmoji}>🤖</div>
|
|
163
|
-
<Text variant="title-small" color="secondary" weight="bold">No assets found</Text>
|
|
164
|
-
<Text variant="body-small" color="secondary" style={{ marginTop: 'var(--bui-space-1)' }}>
|
|
165
|
-
Try adjusting your filters or search terms.
|
|
166
|
-
</Text>
|
|
167
|
-
</div>
|
|
168
|
-
)}
|
|
169
|
-
|
|
170
|
-
{/* Pagination */}
|
|
171
|
-
{totalPages > 1 && (
|
|
172
|
-
<Flex className={styles.paginationRow}>
|
|
173
|
-
<TablePagination
|
|
174
|
-
pageSize={PAGE_SIZE}
|
|
175
|
-
offset={(page - 1) * PAGE_SIZE}
|
|
176
|
-
totalCount={result?.totalCount ?? 0}
|
|
177
|
-
hasNextPage={page < totalPages}
|
|
178
|
-
hasPreviousPage={page > 1}
|
|
179
|
-
onNextPage={() => setPage(p => p + 1)}
|
|
180
|
-
onPreviousPage={() => setPage(p => p - 1)}
|
|
181
|
-
/>
|
|
182
|
-
</Flex>
|
|
183
|
-
)}
|
|
184
|
-
</div>
|
|
185
|
-
|
|
186
|
-
<AssetDetailPanel
|
|
187
|
-
assetId={selectedAssetId}
|
|
188
|
-
onClose={handleCloseDetail}
|
|
189
|
-
/>
|
|
190
|
-
|
|
191
|
-
<AssetInstallDialog
|
|
192
|
-
assetId={installAssetId}
|
|
193
|
-
onClose={handleCloseInstall}
|
|
194
|
-
/>
|
|
195
|
-
</div>
|
|
196
|
-
);
|
|
197
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { DevAiHubPage } from './DevAiHubPage';
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
@layer components {
|
|
2
|
-
.subtitle {
|
|
3
|
-
margin-top: var(--bui-space-1);
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
.tabBar {
|
|
7
|
-
margin-bottom: var(--bui-space-4);
|
|
8
|
-
border-bottom: 1px solid var(--bui-border-1);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
.tabContent {
|
|
12
|
-
display: flex;
|
|
13
|
-
align-items: center;
|
|
14
|
-
gap: var(--bui-space-2);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.sectionLabel {
|
|
18
|
-
text-transform: uppercase;
|
|
19
|
-
letter-spacing: 0.5px;
|
|
20
|
-
display: block;
|
|
21
|
-
margin-bottom: 6px;
|
|
22
|
-
font-weight: 600;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
.providerSection {
|
|
26
|
-
margin-bottom: var(--bui-space-4);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.chipsRow {
|
|
30
|
-
display: flex;
|
|
31
|
-
gap: 6px;
|
|
32
|
-
flex-wrap: wrap;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
.filterChip {
|
|
36
|
-
font-weight: 600;
|
|
37
|
-
font-size: 0.75rem;
|
|
38
|
-
border-radius: var(--bui-radius-2);
|
|
39
|
-
border: 1.5px solid;
|
|
40
|
-
transition: all 0.15s ease;
|
|
41
|
-
cursor: pointer;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
.proactiveSection {
|
|
45
|
-
margin-bottom: var(--bui-space-4);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
.proactiveRow {
|
|
49
|
-
display: flex;
|
|
50
|
-
align-items: flex-start;
|
|
51
|
-
gap: var(--bui-space-2);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.urlSection {
|
|
55
|
-
margin-bottom: var(--bui-space-4);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
.urlBox {
|
|
59
|
-
display: flex;
|
|
60
|
-
align-items: center;
|
|
61
|
-
gap: var(--bui-space-2);
|
|
62
|
-
margin-top: var(--bui-space-1);
|
|
63
|
-
padding: var(--bui-space-2) var(--bui-space-3);
|
|
64
|
-
border-radius: var(--bui-radius-2);
|
|
65
|
-
border: 1px solid var(--bui-border-1);
|
|
66
|
-
background-color: var(--bui-bg-neutral-1-hover);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
.urlText {
|
|
70
|
-
flex: 1;
|
|
71
|
-
font-family: monospace;
|
|
72
|
-
font-size: 0.8rem;
|
|
73
|
-
word-break: break-all;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
.snippetContainer {
|
|
77
|
-
position: relative;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.snippetPre {
|
|
81
|
-
margin: 0;
|
|
82
|
-
padding: var(--bui-space-4);
|
|
83
|
-
border-radius: var(--bui-radius-2);
|
|
84
|
-
border: 1px solid var(--bui-border-1);
|
|
85
|
-
font-family: monospace;
|
|
86
|
-
font-size: 0.8rem;
|
|
87
|
-
overflow-x: auto;
|
|
88
|
-
white-space: pre;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.snippetPreLight {
|
|
92
|
-
background-color: #f6f8fa;
|
|
93
|
-
color: #24292f;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
.snippetPreDark {
|
|
97
|
-
background-color: #1E1E1E;
|
|
98
|
-
color: #DCDDE1;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.copySnippetButton {
|
|
102
|
-
position: absolute;
|
|
103
|
-
top: 8px;
|
|
104
|
-
right: 8px;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
.hint {
|
|
108
|
-
display: block;
|
|
109
|
-
margin-top: var(--bui-space-3);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
@@ -1,271 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
Box, Flex, Text, Button, ButtonIcon, ToggleButton, Switch,
|
|
4
|
-
Dialog, DialogTrigger, DialogHeader, DialogBody, DialogFooter,
|
|
5
|
-
Tabs, TabList, Tab, TabPanel,
|
|
6
|
-
Tooltip, TooltipTrigger,
|
|
7
|
-
} from '@backstage/ui';
|
|
8
|
-
import { RiFileCopyLine, RiCheckLine, RiDatabase2Line } from '@remixicon/react';
|
|
9
|
-
import { useApi, discoveryApiRef } from '@backstage/core-plugin-api';
|
|
10
|
-
import { ToolIcon } from '../ToolIcon';
|
|
11
|
-
import { useCopyToClipboard, useProviders } from '../../hooks';
|
|
12
|
-
import type { AiTool } from '@nospt/plugin-dev-ai-hub-common';
|
|
13
|
-
import styles from './McpConfigDialog.module.css';
|
|
14
|
-
|
|
15
|
-
interface ToolConfig {
|
|
16
|
-
tool: AiTool;
|
|
17
|
-
label: string;
|
|
18
|
-
file: string;
|
|
19
|
-
description: string;
|
|
20
|
-
buildConfig: (mcpUrl: string) => string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const TOOL_CONFIGS: ToolConfig[] = [
|
|
24
|
-
{
|
|
25
|
-
tool: 'claude-code',
|
|
26
|
-
label: 'Claude Code',
|
|
27
|
-
file: '.mcp.json',
|
|
28
|
-
description: 'Add to .mcp.json in your project root (or run via claude mcp add):',
|
|
29
|
-
buildConfig: url => JSON.stringify({
|
|
30
|
-
mcpServers: { 'dev-ai-hub': { type: 'http', url } },
|
|
31
|
-
}, null, 2),
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
tool: 'github-copilot',
|
|
35
|
-
label: 'GitHub Copilot',
|
|
36
|
-
file: '.vscode/settings.json',
|
|
37
|
-
description: 'Add to your VS Code settings (.vscode/settings.json or user settings):',
|
|
38
|
-
buildConfig: url => JSON.stringify({
|
|
39
|
-
'github.copilot.chat.mcp.servers': { 'dev-ai-hub': { type: 'http', url } },
|
|
40
|
-
}, null, 2),
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
tool: 'google-gemini',
|
|
44
|
-
label: 'Google Gemini',
|
|
45
|
-
file: 'gemini-config.json',
|
|
46
|
-
description: 'Add to your Gemini CLI configuration:',
|
|
47
|
-
buildConfig: url => JSON.stringify({
|
|
48
|
-
mcpServers: { 'dev-ai-hub': { url } },
|
|
49
|
-
}, null, 2),
|
|
50
|
-
},
|
|
51
|
-
];
|
|
52
|
-
|
|
53
|
-
function providerLabel(target: string): string {
|
|
54
|
-
return target.split('/').pop()?.replace(/\.git$/, '') ?? target;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
interface McpConfigDialogProps {
|
|
58
|
-
open: boolean;
|
|
59
|
-
onClose: () => void;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function McpConfigDialog({ open, onClose }: McpConfigDialogProps) {
|
|
63
|
-
const discoveryApi = useApi(discoveryApiRef);
|
|
64
|
-
const { copy: copyUrl, copied: copiedUrl } = useCopyToClipboard();
|
|
65
|
-
const { copy: copySnippet, copied: copiedSnippet } = useCopyToClipboard();
|
|
66
|
-
const [selectedToolKey, setSelectedToolKey] = useState<string>('claude-code');
|
|
67
|
-
const [baseUrl, setBaseUrl] = useState('');
|
|
68
|
-
const [selectedProvider, setSelectedProvider] = useState<string>('');
|
|
69
|
-
const [proactiveEnabled, setProactiveEnabled] = useState(false);
|
|
70
|
-
|
|
71
|
-
const { providers } = useProviders();
|
|
72
|
-
const showProviderFilter = providers.length > 1;
|
|
73
|
-
|
|
74
|
-
useEffect(() => {
|
|
75
|
-
if (open) {
|
|
76
|
-
discoveryApi.getBaseUrl('dev-ai-hub').then(url => setBaseUrl(url));
|
|
77
|
-
}
|
|
78
|
-
}, [open, discoveryApi]);
|
|
79
|
-
|
|
80
|
-
const cfg = TOOL_CONFIGS.find(t => t.tool === selectedToolKey) ?? TOOL_CONFIGS[0];
|
|
81
|
-
|
|
82
|
-
const buildMcpUrl = () => {
|
|
83
|
-
if (!baseUrl) return 'loading...';
|
|
84
|
-
const params = new URLSearchParams();
|
|
85
|
-
params.set('tool', cfg.tool);
|
|
86
|
-
if (selectedProvider) params.set('provider', selectedProvider);
|
|
87
|
-
if (proactiveEnabled) params.set('proactive', 'true');
|
|
88
|
-
return `${baseUrl}/mcp?${params.toString()}`;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const mcpUrl = buildMcpUrl();
|
|
92
|
-
const configSnippet = baseUrl ? cfg.buildConfig(mcpUrl) : '';
|
|
93
|
-
|
|
94
|
-
// Detect dark mode by observing BUI's data-theme-mode attribute, which
|
|
95
|
-
// reflects the Backstage theme toggle (not the OS preference).
|
|
96
|
-
const [isDark, setIsDark] = useState(() => {
|
|
97
|
-
if (typeof document === 'undefined') return false;
|
|
98
|
-
return document.documentElement.getAttribute('data-theme-mode') === 'dark' ||
|
|
99
|
-
document.body.getAttribute('data-theme-mode') === 'dark';
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
const getIsDark = () =>
|
|
104
|
-
document.documentElement.getAttribute('data-theme-mode') === 'dark' ||
|
|
105
|
-
document.body.getAttribute('data-theme-mode') === 'dark';
|
|
106
|
-
|
|
107
|
-
const observer = new MutationObserver(() => setIsDark(getIsDark()));
|
|
108
|
-
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme-mode'] });
|
|
109
|
-
observer.observe(document.body, { attributes: true, attributeFilter: ['data-theme-mode'] });
|
|
110
|
-
return () => observer.disconnect();
|
|
111
|
-
}, []);
|
|
112
|
-
|
|
113
|
-
return (
|
|
114
|
-
<DialogTrigger>
|
|
115
|
-
<Dialog
|
|
116
|
-
isOpen={open}
|
|
117
|
-
isDismissable
|
|
118
|
-
onOpenChange={o => { if (!o) onClose(); }}
|
|
119
|
-
>
|
|
120
|
-
<DialogHeader>
|
|
121
|
-
<Text variant="title-small" weight="bold">Configure MCP Server</Text>
|
|
122
|
-
<Text variant="body-small" color="secondary" className={styles.subtitle}>
|
|
123
|
-
Connect your AI tool to the Dev AI Hub via Model Context Protocol.
|
|
124
|
-
</Text>
|
|
125
|
-
</DialogHeader>
|
|
126
|
-
|
|
127
|
-
<DialogBody>
|
|
128
|
-
<Tabs selectedKey={selectedToolKey} onSelectionChange={key => setSelectedToolKey(key as string)}>
|
|
129
|
-
<TabList className={styles.tabBar}>
|
|
130
|
-
{TOOL_CONFIGS.map(t => (
|
|
131
|
-
<Tab key={t.tool} id={t.tool}>
|
|
132
|
-
<span className={styles.tabContent}>
|
|
133
|
-
<ToolIcon tool={t.tool} size={16} />
|
|
134
|
-
<span>{t.label}</span>
|
|
135
|
-
</span>
|
|
136
|
-
</Tab>
|
|
137
|
-
))}
|
|
138
|
-
</TabList>
|
|
139
|
-
|
|
140
|
-
{/* All tab panels share the same content below */}
|
|
141
|
-
{TOOL_CONFIGS.map(t => (
|
|
142
|
-
<TabPanel key={t.tool} id={t.tool}>
|
|
143
|
-
{/* Provider filter — only shown when there are 2+ providers */}
|
|
144
|
-
{showProviderFilter && (
|
|
145
|
-
<Box className={styles.providerSection}>
|
|
146
|
-
<Text variant="body-x-small" color="secondary" className={styles.sectionLabel}>
|
|
147
|
-
Scope to Provider
|
|
148
|
-
</Text>
|
|
149
|
-
<Flex className={styles.chipsRow}>
|
|
150
|
-
<ToggleButton
|
|
151
|
-
id="all-providers"
|
|
152
|
-
size="small"
|
|
153
|
-
className={styles.filterChip}
|
|
154
|
-
isSelected={!selectedProvider}
|
|
155
|
-
onChange={() => setSelectedProvider('')}
|
|
156
|
-
style={{
|
|
157
|
-
borderColor: !selectedProvider ? 'var(--bui-fg-primary)' : 'var(--bui-border-1)',
|
|
158
|
-
backgroundColor: !selectedProvider ? 'var(--bui-fg-primary)' : 'transparent',
|
|
159
|
-
color: !selectedProvider ? 'var(--bui-bg-neutral-1)' : 'var(--bui-fg-secondary)',
|
|
160
|
-
}}
|
|
161
|
-
>
|
|
162
|
-
All providers
|
|
163
|
-
</ToggleButton>
|
|
164
|
-
{providers.map(p => {
|
|
165
|
-
const isSelected = selectedProvider === p.id;
|
|
166
|
-
const label = providerLabel(p.target);
|
|
167
|
-
return (
|
|
168
|
-
<ToggleButton
|
|
169
|
-
key={p.id}
|
|
170
|
-
id={p.id}
|
|
171
|
-
size="small"
|
|
172
|
-
className={styles.filterChip}
|
|
173
|
-
iconStart={<RiDatabase2Line size={12} style={{ color: isSelected ? 'var(--bui-bg-neutral-1)' : 'inherit' }} />}
|
|
174
|
-
isSelected={isSelected}
|
|
175
|
-
onChange={() => setSelectedProvider(isSelected ? '' : p.id)}
|
|
176
|
-
style={{
|
|
177
|
-
borderColor: isSelected ? 'var(--bui-fg-primary)' : 'var(--bui-border-1)',
|
|
178
|
-
backgroundColor: isSelected ? 'var(--bui-fg-primary)' : 'transparent',
|
|
179
|
-
color: isSelected ? 'var(--bui-bg-neutral-1)' : 'var(--bui-fg-secondary)',
|
|
180
|
-
}}
|
|
181
|
-
>
|
|
182
|
-
{label}
|
|
183
|
-
</ToggleButton>
|
|
184
|
-
);
|
|
185
|
-
})}
|
|
186
|
-
</Flex>
|
|
187
|
-
</Box>
|
|
188
|
-
)}
|
|
189
|
-
|
|
190
|
-
{/* Proactive suggestions toggle */}
|
|
191
|
-
<Flex className={styles.proactiveSection}>
|
|
192
|
-
<Flex className={styles.proactiveRow}>
|
|
193
|
-
<Switch
|
|
194
|
-
isSelected={proactiveEnabled}
|
|
195
|
-
onChange={setProactiveEnabled}
|
|
196
|
-
aria-label="Proactive suggestions"
|
|
197
|
-
/>
|
|
198
|
-
<Box>
|
|
199
|
-
<Text variant="body-small" weight="bold">Proactive suggestions</Text>
|
|
200
|
-
<Text variant="body-x-small" color="secondary">
|
|
201
|
-
The AI will automatically suggest relevant assets based on your project context.
|
|
202
|
-
Disable if you prefer to search manually.
|
|
203
|
-
</Text>
|
|
204
|
-
</Box>
|
|
205
|
-
</Flex>
|
|
206
|
-
</Flex>
|
|
207
|
-
|
|
208
|
-
{/* MCP URL */}
|
|
209
|
-
<Box className={styles.urlSection}>
|
|
210
|
-
<Text variant="body-x-small" color="secondary" className={styles.sectionLabel}>
|
|
211
|
-
MCP Endpoint
|
|
212
|
-
</Text>
|
|
213
|
-
<Flex className={styles.urlBox}>
|
|
214
|
-
<Text variant="body-small" className={styles.urlText}>
|
|
215
|
-
{mcpUrl}
|
|
216
|
-
</Text>
|
|
217
|
-
<TooltipTrigger>
|
|
218
|
-
<ButtonIcon
|
|
219
|
-
aria-label="Copy URL"
|
|
220
|
-
icon={copiedUrl ? <RiCheckLine size={14} style={{ color: 'var(--bui-fg-success)' }} /> : <RiFileCopyLine size={14} />}
|
|
221
|
-
variant="tertiary"
|
|
222
|
-
onPress={() => copyUrl(mcpUrl)}
|
|
223
|
-
/>
|
|
224
|
-
<Tooltip>{copiedUrl ? 'Copied!' : 'Copy URL'}</Tooltip>
|
|
225
|
-
</TooltipTrigger>
|
|
226
|
-
</Flex>
|
|
227
|
-
</Box>
|
|
228
|
-
|
|
229
|
-
{/* Config snippet */}
|
|
230
|
-
<Text variant="body-x-small" color="secondary" className={styles.sectionLabel}>
|
|
231
|
-
{t.file}
|
|
232
|
-
</Text>
|
|
233
|
-
<Text variant="body-x-small" color="secondary" style={{ display: 'block', marginBottom: 'var(--bui-space-2)', marginTop: 'var(--bui-space-1)' }}>
|
|
234
|
-
{t.description}
|
|
235
|
-
</Text>
|
|
236
|
-
<div className={styles.snippetContainer}>
|
|
237
|
-
<pre className={`${styles.snippetPre} ${isDark ? styles.snippetPreDark : styles.snippetPreLight}`}>
|
|
238
|
-
{configSnippet}
|
|
239
|
-
</pre>
|
|
240
|
-
<TooltipTrigger>
|
|
241
|
-
<ButtonIcon
|
|
242
|
-
aria-label="Copy config"
|
|
243
|
-
className={styles.copySnippetButton}
|
|
244
|
-
icon={copiedSnippet ? <RiCheckLine size={14} style={{ color: 'var(--bui-fg-success)' }} /> : <RiFileCopyLine size={14} />}
|
|
245
|
-
variant="tertiary"
|
|
246
|
-
onPress={() => copySnippet(configSnippet)}
|
|
247
|
-
/>
|
|
248
|
-
<Tooltip>{copiedSnippet ? 'Copied!' : 'Copy config'}</Tooltip>
|
|
249
|
-
</TooltipTrigger>
|
|
250
|
-
</div>
|
|
251
|
-
|
|
252
|
-
<Text variant="body-x-small" color="secondary" className={styles.hint}>
|
|
253
|
-
💡 Omit <code>?tool=</code> from the URL to receive assets for all AI tools.
|
|
254
|
-
{showProviderFilter && ' Omit ?provider= to receive assets from all repositories.'}
|
|
255
|
-
{' '}Proactive suggestions add <code>?proactive=true</code> and register the{' '}
|
|
256
|
-
<code>suggest_assets</code> tool and <code>check_for_assets</code> prompt.
|
|
257
|
-
</Text>
|
|
258
|
-
</TabPanel>
|
|
259
|
-
))}
|
|
260
|
-
</Tabs>
|
|
261
|
-
</DialogBody>
|
|
262
|
-
|
|
263
|
-
<DialogFooter>
|
|
264
|
-
<Button onClick={onClose} variant="secondary" slot="close">
|
|
265
|
-
Close
|
|
266
|
-
</Button>
|
|
267
|
-
</DialogFooter>
|
|
268
|
-
</Dialog>
|
|
269
|
-
</DialogTrigger>
|
|
270
|
-
);
|
|
271
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { McpConfigDialog } from './McpConfigDialog';
|