Skip to content

Commit 9caafe5

Browse files
authored
Merge pull request #40 from aaronnickovich/diff-tool-feature
feat: add diff tool
2 parents 672c676 + b6ea480 commit 9caafe5

File tree

6 files changed

+265
-3
lines changed

6 files changed

+265
-3
lines changed

plugins/toolbox/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"@material-ui/core": "^4.12.2",
3838
"@material-ui/icons": "^4.9.1",
3939
"@material-ui/lab": "4.0.0-alpha.57",
40+
"@monaco-editor/react": "^4.5.1",
41+
"monaco-editor": "^0.38.0",
4042
"@roadiehq/roadie-backstage-entity-validator": "^2.1.2",
4143
"color-convert": "^2.0.1",
4244
"crypto-hash": "^2.0.1",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {Button, Tooltip} from "@material-ui/core";
2+
import AttachFile from "@material-ui/icons/AttachFile";
3+
import React from "react";
4+
5+
type Props = {
6+
onFileLoad: (input: File) => void;
7+
id: string;
8+
buttonText?: string;
9+
};
10+
11+
export const FileUploadButton = (props: Props) => {
12+
const { onFileLoad, id, buttonText = "Upload File" } = props;
13+
14+
return (
15+
<>
16+
<Tooltip arrow title="Upload File">
17+
<label htmlFor={id}>
18+
<Button
19+
component="span"
20+
size="small"
21+
startIcon={<AttachFile />}
22+
>
23+
{buttonText}
24+
</Button>
25+
</label>
26+
</Tooltip>
27+
<input
28+
type="file"
29+
accept="*/*"
30+
id={id}
31+
hidden
32+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
33+
if (!e?.target?.files?.length) {
34+
return null;
35+
}
36+
return onFileLoad(e.target.files[0]);
37+
}}
38+
/>
39+
</>
40+
);
41+
};

plugins/toolbox/src/components/Buttons/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export { CopyToClipboardButton } from './CopyToClipboardButton';
33
export { FavoriteButton } from './FavoriteButton';
44
export { PasteFromClipboardButton } from './PasteFromClipboardButton';
55
export { SampleButton } from './SampleButton';
6+
export { FileUploadButton } from './FileUploadButton';
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { DiffEditor } from '@monaco-editor/react';
2+
import React, { useEffect, useState } from 'react';
3+
import {Button, ButtonGroup, FormControl, Grid, Tooltip} from '@material-ui/core';
4+
import { useStyles } from '../../utils/hooks';
5+
import * as monaco from 'monaco-editor';
6+
import { useEffectOnce } from 'react-use';
7+
import { Select, SelectItem } from '@backstage/core-components';
8+
9+
import {
10+
ClearValueButton,
11+
FileUploadButton,
12+
CopyToClipboardButton,
13+
PasteFromClipboardButton
14+
} from "../Buttons";
15+
import Input from "@material-ui/icons/Input";
16+
17+
export type MonacoLanguages = { name: string; extensions: string[] };
18+
19+
type SampleButtonProps = {
20+
sample: string[];
21+
setInput: (input: string[]) => void;
22+
};
23+
24+
const options: monaco.editor.IDiffEditorConstructionOptions = {
25+
originalEditable: true,
26+
diffCodeLens: true,
27+
dragAndDrop: true,
28+
tabCompletion: 'on',
29+
renderSideBySide: true,
30+
};
31+
32+
function getLanguage(allowedLanguages: MonacoLanguages[], extension: string) {
33+
return allowedLanguages.find(monacoLanguage =>
34+
monacoLanguage.extensions.includes(extension as string))?.name;
35+
}
36+
37+
function readFileAndSetText(
38+
file: File | undefined,
39+
setText: (value: ((prevState: string) => string) | string) => void,
40+
setLanguage: (value: ((prevState: string) => string) | string) => void,
41+
allowedLanguages: MonacoLanguages[],
42+
) {
43+
if(!file) {
44+
return;
45+
}
46+
47+
const reader = new FileReader();
48+
reader.onload = async e => {
49+
// @ts-ignore
50+
setText(e.target.result);
51+
};
52+
reader.readAsText(file);
53+
let newLanguage = 'plaintext';
54+
const extension = `.${file.name.split('.').pop()}`;
55+
if (allowedLanguages?.length) {
56+
newLanguage = getLanguage(allowedLanguages, extension) || newLanguage;
57+
}
58+
setLanguage(newLanguage);
59+
}
60+
61+
export const SampleButton = (props: SampleButtonProps) => {
62+
const { sample, setInput } = props;
63+
return (
64+
<Tooltip arrow title="Input sample">
65+
<Button
66+
size="small"
67+
startIcon={<Input />}
68+
onClick={() => setInput(sample)}
69+
>
70+
Sample
71+
</Button>
72+
</Tooltip>
73+
);
74+
};
75+
76+
function Diff() {
77+
const styles = useStyles();
78+
const [originalFile, setOriginalFile] = useState<File>();
79+
const [modifiedFile, setModifiedFile] = useState<File>();
80+
81+
const [originalText, setOriginalText] = useState('');
82+
const [modifiedText, setModifiedText] = useState('');
83+
84+
const [language, setLanguage] = useState('plaintext');
85+
const [allowedLanguages, setAllowedLanguages] = useState<MonacoLanguages[]>([],);
86+
87+
const exampleOriginalText = 'Backstage toolbox\n\ncompare text';
88+
const exampleModifiedText = 'Backstage toolbox\ndiff editor';
89+
const handleLanguageSelect = (selected: any) => {
90+
setLanguage(selected);
91+
};
92+
93+
useEffect(() => {
94+
readFileAndSetText(
95+
modifiedFile,
96+
setModifiedText,
97+
setLanguage,
98+
allowedLanguages,
99+
);
100+
}, [modifiedFile, allowedLanguages]);
101+
102+
useEffect(() => {
103+
readFileAndSetText(
104+
originalFile,
105+
setOriginalText,
106+
setLanguage,
107+
allowedLanguages,
108+
);
109+
}, [originalFile, allowedLanguages]);
110+
111+
useEffectOnce(() => {
112+
const languages: MonacoLanguages[] = monaco.languages
113+
.getLanguages()
114+
.map(each => {
115+
return { name: each.id, extensions: each.extensions || [] };
116+
});
117+
setAllowedLanguages(languages);
118+
});
119+
120+
const languageOptions: SelectItem[] = allowedLanguages
121+
? allowedLanguages.map(i => ({ label: i.name, value: i.name }))
122+
: [{ label: 'Loading...', value: 'loading' }];
123+
124+
return (
125+
<>
126+
<FormControl className={styles.fullWidth}>
127+
<Grid container style={{ width: '100%' }}>
128+
<Grid item style={{ minWidth: '200px' }}>
129+
<Select
130+
selected={language}
131+
onChange={handleLanguageSelect}
132+
items={languageOptions}
133+
label="Select Text Language"
134+
/>
135+
</Grid>
136+
</Grid>
137+
<Grid container style={{ width: '100%' }}>
138+
<Grid item>
139+
{exampleOriginalText && exampleModifiedText &&
140+
<SampleButton
141+
setInput={input => {
142+
setOriginalText( input[0] );
143+
setModifiedText( input[1] );
144+
}}
145+
sample={[exampleOriginalText, exampleModifiedText]}
146+
/>}
147+
</Grid>
148+
</Grid>
149+
<Grid container style={{ marginBottom: '5px', width: '100%' }}>
150+
<Grid item style={{ width: '50%' }}>
151+
<ButtonGroup size="small">
152+
<FileUploadButton
153+
onFileLoad={setOriginalFile}
154+
id="originalFile"
155+
buttonText="Original File"
156+
/>
157+
<ClearValueButton setValue={setOriginalText} />
158+
<PasteFromClipboardButton setInput={setOriginalText} />
159+
{originalText && <CopyToClipboardButton output={originalText} />}
160+
</ButtonGroup>
161+
</Grid>
162+
<Grid item style={{ width: '50%' }}>
163+
<ButtonGroup size="small">
164+
<FileUploadButton
165+
onFileLoad={setModifiedFile}
166+
id="modifiedFile"
167+
buttonText="Modified File"
168+
/>
169+
<ClearValueButton setValue={setModifiedText} />
170+
<PasteFromClipboardButton setInput={setModifiedText} />
171+
{modifiedText && <CopyToClipboardButton output={modifiedText} />}
172+
</ButtonGroup>
173+
</Grid>
174+
</Grid>
175+
<DiffEditor
176+
height="100vh"
177+
original={originalText}
178+
modified={modifiedText}
179+
options={options}
180+
language={language}
181+
/>
182+
</FormControl>
183+
</>
184+
);
185+
}
186+
187+
export default Diff;

plugins/toolbox/src/components/Root/tools.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const EntityValidator = lazy(() => import('../Validators/EntityValidator'));
2525
const EntityDescriber = lazy(() => import('../Misc/EntityDescriber'));
2626
const Countdown = lazy(() => import('../Misc/Countdown'));
2727
const Timer = lazy(() => import('../Misc/Timer'));
28+
const Diff = lazy(() => import('../Misc/Diff'));
2829

2930
const LoremIpsum = lazy(() => import('../Generators/LoremIpsum'));
3031
const Hash = lazy(() => import('../Generators/Hash'));
@@ -204,6 +205,12 @@ export const defaultTools: Tool[] = [
204205
component: <Timer />,
205206
category: 'Miscellaneous',
206207
},
208+
{
209+
id: 'diff',
210+
name: 'File Diff',
211+
component: <Diff />,
212+
category: 'Miscellaneous',
213+
},
207214
/**
208215
{
209216
id: 'cidr-calculator',

yarn.lock

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2462,6 +2462,20 @@
24622462
prop-types "^15.7.2"
24632463
react-is "^16.8.0 || ^17.0.0"
24642464

2465+
"@monaco-editor/loader@^1.3.3":
2466+
version "1.3.3"
2467+
resolved "https://registry.yarnpkg.com/@monaco-editor/loader/-/loader-1.3.3.tgz#7f1742bd3cc21c0362a46a4056317f6e5215cfca"
2468+
integrity sha512-6KKF4CTzcJiS8BJwtxtfyYt9shBiEv32ateQ9T4UVogwn4HM/uPo9iJd2Dmbkpz8CM6Y0PDUpjnZzCwC+eYo2Q==
2469+
dependencies:
2470+
state-local "^1.0.6"
2471+
2472+
"@monaco-editor/react@^4.5.1":
2473+
version "4.5.1"
2474+
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.5.1.tgz#fbc76c692aee9a33b9ab24ae0c5f219b8f002fdb"
2475+
integrity sha512-NNDFdP+2HojtNhCkRfE6/D6ro6pBNihaOzMbGK84lNWzRu+CfBjwzGt4jmnqimLuqp5yE5viHS2vi+QOAnD5FQ==
2476+
dependencies:
2477+
"@monaco-editor/loader" "^1.3.3"
2478+
24652479
"@mswjs/cookies@^0.2.2":
24662480
version "0.2.2"
24672481
resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-0.2.2.tgz#b4e207bf6989e5d5427539c2443380a33ebb922b"
@@ -3526,9 +3540,9 @@
35263540
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
35273541

35283542
"@types/react-dom@<18.0.0", "@types/react-dom@^17":
3529-
version "17.0.19"
3530-
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.19.tgz#36feef3aa35d045cacd5ed60fe0eef5272f19492"
3531-
integrity sha512-PiYG40pnQRdPHnlf7tZnp0aQ6q9tspYr72vD61saO6zFCybLfMqwUCN0va1/P+86DXn18ZWeW30Bk7xlC5eEAQ==
3543+
version "17.0.20"
3544+
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.20.tgz#e0c8901469d732b36d8473b40b679ad899da1b53"
3545+
integrity sha512-4pzIjSxDueZZ90F52mU3aPoogkHIoSIDG+oQ+wQK7Cy2B9S+MvOqY0uEA/qawKz381qrEDkvpwyt8Bm31I8sbA==
35323546
dependencies:
35333547
"@types/react" "^17"
35343548

@@ -9922,6 +9936,11 @@ modify-values@^1.0.0:
99229936
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
99239937
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
99249938

9939+
monaco-editor@^0.38.0:
9940+
version "0.38.0"
9941+
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.38.0.tgz#7b3cd16f89b1b8867fcd3c96e67fccee791ff05c"
9942+
integrity sha512-11Fkh6yzEmwx7O0YoLxeae0qEGFwmyPRlVxpg7oF9czOOCB/iCjdJrG5I67da5WiXK3YJCxoz9TJFE8Tfq/v9A==
9943+
99259944
moo@^0.5.0:
99269945
version "0.5.2"
99279946
resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c"
@@ -12440,6 +12459,11 @@ standard-version@^9.5.0:
1244012459
stringify-package "^1.0.1"
1244112460
yargs "^16.0.0"
1244212461

12462+
state-local@^1.0.6:
12463+
version "1.0.7"
12464+
resolved "https://registry.yarnpkg.com/state-local/-/state-local-1.0.7.tgz#da50211d07f05748d53009bee46307a37db386d5"
12465+
integrity sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==
12466+
1244312467
statuses@2.0.1:
1244412468
version "2.0.1"
1244512469
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"

0 commit comments

Comments
 (0)