main
tsx 206 lines 6.2 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react';
9 import type {editor} from 'monaco-editor';
10 import * as monaco from 'monaco-editor';
11 import React, {
12 useState,
13 useRef,
14 unstable_ViewTransition as ViewTransition,
15 unstable_addTransitionType as addTransitionType,
16 startTransition,
17 } from 'react';
18 import {Resizable} from 're-resizable';
19 import {useStore, useStoreDispatch} from '../StoreContext';
20 import {monacoConfigOptions} from './monacoOptions';
21 import {IconChevron} from '../Icons/IconChevron';
22 import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes';
23
24 loader.config({monaco});
25
26 export default function ConfigEditor({
27 formattedAppliedConfig,
28 }: {
29 formattedAppliedConfig: string;
30 }): React.ReactElement {
31 const [isExpanded, setIsExpanded] = useState(false);
32
33 // TODO: Add back <Activity> after upgrading next.js
34 return (
35 <>
36 <div
37 style={{
38 display: isExpanded ? 'block' : 'none',
39 }}>
40 {/* <Activity mode={isExpanded ? 'visible' : 'hidden'}> */}
41 <ExpandedEditor
42 onToggle={() => {
43 startTransition(() => {
44 addTransitionType(CONFIG_PANEL_TRANSITION);
45 setIsExpanded(false);
46 });
47 }}
48 formattedAppliedConfig={formattedAppliedConfig}
49 />
50 </div>
51 <div
52 style={{
53 display: !isExpanded ? 'block' : 'none',
54 }}>
55 {/* </Activity>
56 <Activity mode={isExpanded ? 'hidden' : 'visible'}></Activity> */}
57 <CollapsedEditor
58 onToggle={() => {
59 startTransition(() => {
60 addTransitionType(CONFIG_PANEL_TRANSITION);
61 setIsExpanded(true);
62 });
63 }}
64 />
65 </div>
66 {/* </Activity> */}
67 </>
68 );
69 }
70
71 function ExpandedEditor({
72 onToggle,
73 formattedAppliedConfig,
74 }: {
75 onToggle: (expanded: boolean) => void;
76 formattedAppliedConfig: string;
77 }): React.ReactElement {
78 const store = useStore();
79 const dispatchStore = useStoreDispatch();
80 const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
81
82 const handleChange: (value: string | undefined) => void = (
83 value: string | undefined,
84 ) => {
85 if (value === undefined) return;
86
87 if (debounceTimerRef.current) {
88 clearTimeout(debounceTimerRef.current);
89 }
90
91 debounceTimerRef.current = setTimeout(() => {
92 dispatchStore({
93 type: 'updateConfig',
94 payload: {
95 config: value,
96 },
97 });
98 }, 500); // 500ms debounce delay
99 };
100
101 const handleMount: (
102 _: editor.IStandaloneCodeEditor,
103 monaco: Monaco,
104 ) => void = (_, monaco) => {
105 // Enable comments in JSON for JSON5-style config
106 monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
107 allowComments: true,
108 trailingCommas: 'ignore',
109 });
110 };
111
112 return (
113 <ViewTransition
114 update={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}>
115 {/* enter={{[CONFIG_PANEL_TRANSITION]: 'slide-in', default: 'none'}}
116 exit={{[CONFIG_PANEL_TRANSITION]: 'slide-out', default: 'none'}}> */}
117 <Resizable
118 minWidth={300}
119 maxWidth={600}
120 defaultSize={{width: 350}}
121 enable={{right: true, bottom: false}}>
122 <div className="bg-blue-10 relative h-full flex flex-col !h-[calc(100vh_-_3.5rem)] border border-gray-300">
123 <div
124 className="absolute w-8 h-16 bg-blue-10 rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-l-0 border-gray-300"
125 title="Minimize config editor"
126 onClick={onToggle}
127 style={{
128 top: '50%',
129 marginTop: '-32px',
130 right: '-32px',
131 borderTopLeftRadius: 0,
132 borderBottomLeftRadius: 0,
133 }}>
134 <IconChevron displayDirection="left" className="text-blue-50" />
135 </div>
136
137 <div className="flex-1 flex flex-col m-2 mb-2">
138 <div className="pb-2">
139 <h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
140 Config Overrides
141 </h2>
142 </div>
143 <div className="flex-1 border border-gray-300">
144 <MonacoEditor
145 path={'config.json5'}
146 language={'json'}
147 value={store.config}
148 onMount={handleMount}
149 onChange={handleChange}
150 loading={''}
151 className="monaco-editor-config"
152 options={monacoConfigOptions}
153 />
154 </div>
155 </div>
156 <div className="flex-1 flex flex-col m-2">
157 <div className="pb-2">
158 <h2 className="inline-block text-blue-50 py-1.5 px-1.5 xs:px-3 sm:px-4 text-sm">
159 Applied Configs
160 </h2>
161 </div>
162 <div className="flex-1 border border-gray-300">
163 <MonacoEditor
164 path={'applied-config.js'}
165 language={'javascript'}
166 value={formattedAppliedConfig}
167 loading={''}
168 className="monaco-editor-applied-config"
169 options={{
170 ...monacoConfigOptions,
171 readOnly: true,
172 }}
173 />
174 </div>
175 </div>
176 </div>
177 </Resizable>
178 </ViewTransition>
179 );
180 }
181
182 function CollapsedEditor({
183 onToggle,
184 }: {
185 onToggle: () => void;
186 }): React.ReactElement {
187 return (
188 <div
189 className="w-4 !h-[calc(100vh_-_3.5rem)]"
190 style={{position: 'relative'}}>
191 <div
192 className="absolute w-10 h-16 bg-blue-10 hover:translate-x-2 transition-transform rounded-r-full flex items-center justify-center z-[2] cursor-pointer border border-gray-300"
193 title="Expand config editor"
194 onClick={onToggle}
195 style={{
196 top: '50%',
197 marginTop: '-32px',
198 left: '-8px',
199 borderTopLeftRadius: 0,
200 borderBottomLeftRadius: 0,
201 }}>
202 <IconChevron displayDirection="right" className="text-blue-50" />
203 </div>
204 </div>
205 );
206 }