Skip to content

Commit 9217bc0

Browse files
feat(marketplace): add monaco editor (#500)
Signed-off-by: Christoph Jerolimov <jerolimov+git@redhat.com>
1 parent cc1e7ae commit 9217bc0

6 files changed

Lines changed: 267 additions & 18 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@red-hat-developer-hub/backstage-plugin-marketplace': minor
3+
---
4+
5+
add manaco editor for the upcoming installation page

workspaces/marketplace/packages/cli/src/commands/generate/index.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import fs from 'fs-extra';
1818
import { OptionValues } from 'commander';
1919
import path from 'path';
20-
import YAML from 'yaml';
20+
import yaml from 'yaml';
2121
import {
2222
EXTENSIONS_API_VERSION,
2323
MarketplaceKind,
@@ -51,7 +51,7 @@ export default async (opts: OptionValues) => {
5151
defaultDynamicPluginsPath,
5252
'utf8',
5353
);
54-
const defaultPluginsConfig = YAML.parse(
54+
const defaultPluginsConfig = yaml.parse(
5555
defaultDynamicPluginsContent,
5656
) as DynamicPluginsConfig;
5757

@@ -207,16 +207,16 @@ export default async (opts: OptionValues) => {
207207
for (const entity of entities) {
208208
const filename = `${entity.metadata.name}.yaml`;
209209
const entityPath = path.join(outputDirPath, filename);
210-
await fs.writeFile(entityPath, YAML.stringify(entity));
210+
await fs.writeFile(entityPath, yaml.stringify(entity));
211211
location.spec.targets?.push(`./${filename}`);
212212
}
213213
await fs.writeFile(
214214
path.join(outputDirPath, 'all.yaml'),
215-
YAML.stringify(location),
215+
yaml.stringify(location),
216216
);
217217
} else {
218218
for (const entity of entities) {
219-
console.log(YAML.stringify(entity));
219+
console.log(yaml.stringify(entity));
220220
console.log('---');
221221
}
222222
}

workspaces/marketplace/plugins/marketplace/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,15 @@
3939
"@backstage/core-plugin-api": "^1.10.3",
4040
"@backstage/plugin-catalog-react": "^1.15.1",
4141
"@backstage/theme": "^0.6.3",
42+
"@monaco-editor/react": "^4.7.0",
4243
"@mui/icons-material": "^5.16.7",
4344
"@mui/material": "^5.12.2",
4445
"@red-hat-developer-hub/backstage-plugin-marketplace-common": "workspace:^",
4546
"@scalprum/react-core": "0.9.3",
46-
"@tanstack/react-query": "^5.60.5"
47+
"@tanstack/react-query": "^5.60.5",
48+
"monaco-editor": "^0.52.2",
49+
"react-use": "^17.6.0",
50+
"yaml": "^2.7.0"
4751
},
4852
"peerDependencies": {
4953
"react": "^17.0.0 || ^18.0.0",
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
* Copyright The Backstage Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import React from 'react';
18+
19+
import { Progress } from '@backstage/core-components';
20+
21+
import { useTheme } from '@mui/material/styles';
22+
import Editor, { loader, OnChange, OnMount } from '@monaco-editor/react';
23+
import * as monacoEditor from 'monaco-editor';
24+
import type MonacoEditor from 'monaco-editor';
25+
26+
loader.config({ monaco: monacoEditor });
27+
28+
const defaultOptions: MonacoEditor.editor.IEditorConstructionOptions = {
29+
minimap: { enabled: false },
30+
};
31+
32+
interface CodeEditorContextValue {
33+
getEditor: () => MonacoEditor.editor.ICodeEditor | null;
34+
setEditor: (editor: MonacoEditor.editor.ICodeEditor) => void;
35+
/** short for getEditor()?.getValue() */
36+
getValue: () => string | undefined;
37+
/** short for getEditor()?.setValue() and getEditor()?.focus() */
38+
setValue: (value: string, autoFocus?: boolean) => void;
39+
}
40+
41+
const CodeEditorContext = React.createContext<CodeEditorContextValue>(
42+
undefined as any as CodeEditorContextValue,
43+
);
44+
45+
export const CodeEditorContextProvider = (props: {
46+
children: React.ReactNode;
47+
}) => {
48+
const editorRef = React.useRef<MonacoEditor.editor.ICodeEditor | null>(null);
49+
const contextValue = React.useMemo<CodeEditorContextValue>(
50+
() => ({
51+
getEditor: () => editorRef.current,
52+
setEditor: (editor: MonacoEditor.editor.ICodeEditor) => {
53+
editorRef.current = editor;
54+
},
55+
getValue: () => editorRef.current?.getValue(),
56+
setValue: (value: string, autoFocus = true) => {
57+
editorRef.current?.setValue(value);
58+
if (autoFocus) {
59+
editorRef.current?.focus();
60+
}
61+
},
62+
}),
63+
[],
64+
);
65+
66+
return (
67+
<CodeEditorContext.Provider value={contextValue}>
68+
{props.children}
69+
</CodeEditorContext.Provider>
70+
);
71+
};
72+
73+
export const useCodeEditor = () => {
74+
const contextValue = React.useContext(CodeEditorContext);
75+
if (!contextValue) {
76+
throw new Error(
77+
'useCodeEditor must be used within a CodeEditorContextProvider',
78+
);
79+
}
80+
return contextValue;
81+
};
82+
83+
export interface CodeEditorProps {
84+
defaultLanguage: 'yaml'; // We enforce YAML for now
85+
defaultValue?: string;
86+
onChange?: OnChange;
87+
onLoaded?: () => void;
88+
}
89+
90+
/**
91+
* Wrapper around Editor from @monaco-editor/react.
92+
* It automaticallty sets the editor into the current
93+
* CodeEditorContext so that it can be accessed via useCodeEditor
94+
* and we can have a uncontrolled input component.
95+
*/
96+
export const CodeEditor = ({
97+
defaultLanguage,
98+
onChange,
99+
onLoaded,
100+
...otherProps
101+
}: CodeEditorProps) => {
102+
const theme = useTheme().palette.mode === 'dark' ? 'vs-dark' : 'vs-light';
103+
104+
const codeEditor = useCodeEditor();
105+
106+
const onMount = React.useCallback<OnMount>(
107+
(editor, _monaco) => {
108+
codeEditor.setEditor(editor);
109+
onLoaded?.();
110+
},
111+
[codeEditor, onLoaded],
112+
);
113+
114+
return (
115+
<Editor
116+
theme={theme}
117+
defaultLanguage={defaultLanguage}
118+
onChange={onChange}
119+
onMount={onMount}
120+
loading={<Progress />}
121+
options={defaultOptions}
122+
{...otherProps}
123+
/>
124+
);
125+
};

workspaces/marketplace/plugins/marketplace/src/components/MarketplacePluginInstallContent.tsx

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,86 @@ import React from 'react';
1919
import { ErrorPage, Progress } from '@backstage/core-components';
2020
import { useRouteRefParams } from '@backstage/core-plugin-api';
2121

22-
import { MarketplacePlugin } from '@red-hat-developer-hub/backstage-plugin-marketplace-common';
22+
import Button from '@mui/material/Button';
23+
import yaml from 'yaml';
24+
import { useCopyToClipboard } from 'react-use';
25+
26+
import {
27+
MarketplacePackage,
28+
MarketplacePlugin,
29+
} from '@red-hat-developer-hub/backstage-plugin-marketplace-common';
2330

2431
import { pluginInstallRouteRef } from '../routes';
2532
import { usePlugin } from '../hooks/usePlugin';
33+
import { usePluginPackages } from '../hooks/usePluginPackages';
34+
import {
35+
CodeEditorContextProvider,
36+
CodeEditor,
37+
useCodeEditor,
38+
} from './CodeEditor';
2639

2740
export const MarketplacePluginInstallContent = ({
2841
plugin,
42+
packages,
2943
}: {
3044
plugin: MarketplacePlugin;
45+
packages: MarketplacePackage[];
3146
}) => {
47+
const codeEditor = useCodeEditor();
48+
49+
const onLoaded = React.useCallback(() => {
50+
const dynamicPluginYaml = {
51+
plugins: (packages ?? []).map(pkg => ({
52+
package: pkg.spec?.dynamicArtifact ?? './dynamic-plugins/dist/....',
53+
disabled: false,
54+
})),
55+
};
56+
codeEditor.setValue(yaml.stringify(dynamicPluginYaml));
57+
}, [codeEditor, packages]);
58+
59+
// Just a demo
60+
const showFullPlugin = React.useCallback(() => {
61+
codeEditor.setValue(yaml.stringify(plugin));
62+
}, [codeEditor, plugin]);
63+
64+
const [, copyToClipboard] = useCopyToClipboard();
65+
const handleCopyToClipboard = React.useCallback(() => {
66+
const value = codeEditor.getValue();
67+
if (value) {
68+
copyToClipboard(value);
69+
}
70+
}, [codeEditor, copyToClipboard]);
71+
3272
return (
33-
<div>
34-
<h2>Not implemented yet</h2>
35-
<div>Plugin entity:</div>
36-
<pre>{JSON.stringify(plugin, null, 2)}</pre>
73+
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
74+
<div style={{ flex: 1 }}>
75+
<CodeEditor defaultLanguage="yaml" onLoaded={onLoaded} />
76+
</div>
77+
<div style={{ paddingTop: 16 }}>
78+
<Button
79+
variant="contained"
80+
color="primary"
81+
onClick={onLoaded}
82+
sx={{ mr: 1 }}
83+
>
84+
Reset
85+
</Button>
86+
<Button
87+
variant="contained"
88+
color="primary"
89+
onClick={showFullPlugin}
90+
sx={{ mr: 1 }}
91+
>
92+
Show full plugin
93+
</Button>
94+
<Button
95+
variant="contained"
96+
color="primary"
97+
onClick={handleCopyToClipboard}
98+
>
99+
Copy
100+
</Button>
101+
</div>
37102
</div>
38103
);
39104
};
@@ -42,13 +107,23 @@ export const MarketplacePluginInstallContentLoader = () => {
42107
const params = useRouteRefParams(pluginInstallRouteRef);
43108

44109
const plugin = usePlugin(params.namespace, params.name);
110+
const packages = usePluginPackages(params.namespace, params.name);
45111

46-
if (plugin.isLoading) {
112+
if (plugin.isLoading || packages.isLoading) {
47113
return <Progress />;
48-
} else if (plugin.data) {
49-
return <MarketplacePluginInstallContent plugin={plugin.data} />;
114+
} else if (plugin.data && packages.data) {
115+
return (
116+
<CodeEditorContextProvider>
117+
<MarketplacePluginInstallContent
118+
plugin={plugin.data}
119+
packages={packages.data}
120+
/>
121+
</CodeEditorContextProvider>
122+
);
50123
} else if (plugin.error) {
51124
return <ErrorPage statusMessage={plugin.error.toString()} />;
125+
} else if (packages.error) {
126+
return <ErrorPage statusMessage={packages.error.toString()} />;
52127
}
53128
return (
54129
<ErrorPage

0 commit comments

Comments
 (0)