More UI changes to the playground (#2683)
Changes: ## Get rid of emotion and use pure css modules ## Better UI for errors and warnings Errors and warnings are now an expandable banner at the bottom of the UI, it is completely resizable in the same way as the left and right pane are. <img width="363" alt="image" src="https://github.com/microsoft/typespec/assets/1031227/3c126340-10e4-4dca-9ba2-a76fa9393119"> <img width="336" alt="image" src="https://github.com/microsoft/typespec/assets/1031227/0cef33df-df5e-4017-8d42-d7a8fbcb053f"> <img width="730" alt="image" src="https://github.com/microsoft/typespec/assets/1031227/baab69fe-5ea5-497d-8377-110a809e467a"> ## Output tabs align with the command bar and use fluentui tabs(Makes the tabs accessible) <img width="627" alt="image" src="https://github.com/microsoft/typespec/assets/1031227/90f9ac54-3c02-48f1-a7ed-f85b57e360fd">
This commit is contained in:
Родитель
dbd0149b15
Коммит
fb7de9c832
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"changes": [
|
||||
{
|
||||
"packageName": "@typespec/playground",
|
||||
"comment": "Move errors and warnings to a dedicated expandable banner at the bottom of the playground.",
|
||||
"type": "none"
|
||||
}
|
||||
],
|
||||
"packageName": "@typespec/playground"
|
||||
}
|
|
@ -849,9 +849,6 @@ importers:
|
|||
|
||||
../../packages/playground:
|
||||
dependencies:
|
||||
'@emotion/react':
|
||||
specifier: ^11.11.1
|
||||
version: 11.11.1(@types/react@18.2.34)(react@18.2.0)
|
||||
'@fluentui/react-components':
|
||||
specifier: ~9.32.1
|
||||
version: 9.32.3(@types/react-dom@18.2.14)(@types/react@18.2.34)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.20.2)
|
||||
|
@ -1003,9 +1000,6 @@ importers:
|
|||
|
||||
../../packages/playground-website:
|
||||
dependencies:
|
||||
'@emotion/react':
|
||||
specifier: ^11.11.1
|
||||
version: 11.11.1(@types/react@18.2.34)(react@18.2.0)
|
||||
'@typespec/compiler':
|
||||
specifier: workspace:~0.50.0
|
||||
version: link:../compiler
|
||||
|
|
|
@ -61,7 +61,6 @@
|
|||
"@typespec/openapi": "workspace:~0.50.0",
|
||||
"@typespec/protobuf": "workspace:~0.50.0",
|
||||
"@typespec/html-program-viewer": "workspace:~0.50.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"react-dom": "~18.2.0",
|
||||
"react": "~18.2.0",
|
||||
"es-module-shims": "~1.8.0"
|
||||
|
|
|
@ -14,9 +14,7 @@
|
|||
"jsx": "react-jsx",
|
||||
"checkJs": true,
|
||||
"allowJs": true,
|
||||
"jsxImportSource": "@emotion/react",
|
||||
"lib": ["DOM", "ES2022"],
|
||||
"types": ["@emotion/react"]
|
||||
"lib": ["DOM", "ES2022"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
|
|
@ -40,8 +40,8 @@
|
|||
"default": "./dist/react/index.js"
|
||||
},
|
||||
"./react/viewers": {
|
||||
"types": "./dist/src/react/viewers.d.ts",
|
||||
"default": "./dist/react/viewers.js"
|
||||
"types": "./dist/src/react/viewers/index.d.ts",
|
||||
"default": "./dist/react/viewers/index.js"
|
||||
},
|
||||
"./style.css": "./dist/index.css"
|
||||
},
|
||||
|
@ -68,7 +68,6 @@
|
|||
"!dist/test/**"
|
||||
],
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@fluentui/react-components": "~9.32.1",
|
||||
"@fluentui/react-icons": "~2.0.217",
|
||||
"@typespec/bundler": "workspace:~0.1.0-alpha.3",
|
||||
|
|
|
@ -13,9 +13,9 @@ const dependencies = Object.keys(packageJson.dependencies);
|
|||
const external = [
|
||||
...dependencies,
|
||||
"swagger-ui-react/swagger-ui.css",
|
||||
"@emotion/react/jsx-runtime",
|
||||
"@typespec/bundler/vite",
|
||||
"react-dom/client",
|
||||
"react/jsx-runtime",
|
||||
"vite",
|
||||
"@vitejs/plugin-react",
|
||||
"fs/promises",
|
||||
|
@ -25,7 +25,7 @@ export default defineConfig([
|
|||
input: {
|
||||
index: "src/index.ts",
|
||||
"react/index": "src/react/index.ts",
|
||||
"react/viewers": "src/react/viewers.tsx",
|
||||
"react/viewers/index": "src/react/viewers/index.tsx",
|
||||
"tooling/index": "src/tooling/index.ts",
|
||||
"vite/index": "src/vite/index.ts",
|
||||
},
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
.bar {
|
||||
border-bottom: 1px solid var(--colorNeutralStroke1);
|
||||
}
|
||||
|
||||
.divider {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
width: 10px;
|
||||
}
|
|
@ -1,26 +1,12 @@
|
|||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogSurface,
|
||||
DialogTrigger,
|
||||
Link,
|
||||
Toolbar,
|
||||
ToolbarButton,
|
||||
Tooltip,
|
||||
tokens,
|
||||
} from "@fluentui/react-components";
|
||||
import {
|
||||
Broom16Filled,
|
||||
Bug16Regular,
|
||||
Save16Regular,
|
||||
Settings24Regular,
|
||||
} from "@fluentui/react-icons";
|
||||
import { Link, Toolbar, ToolbarButton, Tooltip } from "@fluentui/react-components";
|
||||
import { Broom16Filled, Bug16Regular, Save16Regular } from "@fluentui/react-icons";
|
||||
import { CompilerOptions } from "@typespec/compiler";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
import { EmitterDropdown } from "../react/emitter-dropdown.js";
|
||||
import { SamplesDropdown } from "../react/samples-dropdown.js";
|
||||
import { CompilerSettingsDialogButton } from "../react/settings/compiler-settings-dialog-button.js";
|
||||
import { BrowserHost, PlaygroundSample } from "../types.js";
|
||||
import { EmitterDropdown } from "./emitter-dropdown.js";
|
||||
import { SamplesDropdown } from "./samples-dropdown.js";
|
||||
import { CompilerSettings } from "./settings/compiler-settings.js";
|
||||
import style from "./editor-command-bar.module.css";
|
||||
|
||||
export interface EditorCommandBarProps {
|
||||
documentationUrl?: string;
|
||||
|
@ -45,7 +31,7 @@ export const EditorCommandBar: FunctionComponent<EditorCommandBarProps> = ({
|
|||
host,
|
||||
selectedEmitter,
|
||||
onSelectedEmitterChange,
|
||||
compilerOptions: emitterOptions,
|
||||
compilerOptions,
|
||||
onCompilerOptionsChange,
|
||||
samples,
|
||||
selectedSampleName,
|
||||
|
@ -70,7 +56,7 @@ export const EditorCommandBar: FunctionComponent<EditorCommandBarProps> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<div css={{ borderBottom: `1px solid ${tokens.colorNeutralStroke1}` }}>
|
||||
<div className={style["bar"]}>
|
||||
<Toolbar>
|
||||
<Tooltip content="Save" relationship="description" withArrow>
|
||||
<ToolbarButton aria-label="Save" icon={<Save16Regular />} onClick={saveCode as any} />
|
||||
|
@ -79,35 +65,34 @@ export const EditorCommandBar: FunctionComponent<EditorCommandBarProps> = ({
|
|||
<ToolbarButton aria-label="Format" icon={<Broom16Filled />} onClick={formatCode as any} />
|
||||
</Tooltip>
|
||||
{samples && (
|
||||
<SamplesDropdown
|
||||
samples={samples}
|
||||
selectedSampleName={selectedSampleName}
|
||||
onSelectedSampleNameChange={onSelectedSampleNameChange}
|
||||
/>
|
||||
<>
|
||||
<SamplesDropdown
|
||||
samples={samples}
|
||||
selectedSampleName={selectedSampleName}
|
||||
onSelectedSampleNameChange={onSelectedSampleNameChange}
|
||||
/>
|
||||
<div className={style["spacer"]}></div>
|
||||
</>
|
||||
)}
|
||||
<EmitterDropdown
|
||||
emitters={emitters}
|
||||
onSelectedEmitterChange={onSelectedEmitterChange}
|
||||
selectedEmitter={selectedEmitter}
|
||||
/>
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<ToolbarButton icon={<Settings24Regular />} />
|
||||
</DialogTrigger>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<CompilerSettings
|
||||
host={host}
|
||||
selectedEmitter={selectedEmitter}
|
||||
options={emitterOptions}
|
||||
onOptionsChanged={onCompilerOptionsChange}
|
||||
/>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
{documentation}
|
||||
<div css={{ flex: "1" }}></div>
|
||||
|
||||
{documentation && (
|
||||
<>
|
||||
<div className={style["spacer"]}></div>
|
||||
{documentation}
|
||||
</>
|
||||
)}
|
||||
<div className={style["divider"]}></div>
|
||||
{bugButton}
|
||||
<CompilerSettingsDialogButton
|
||||
compilerOptions={compilerOptions}
|
||||
onCompilerOptionsChange={onCompilerOptionsChange}
|
||||
selectedEmitter={selectedEmitter}
|
||||
/>
|
||||
</Toolbar>
|
||||
</div>
|
||||
);
|
||||
|
@ -124,9 +109,7 @@ const FileBugButton: FunctionComponent<FileBugButtonProps> = ({ onClick }) => {
|
|||
aria-label="File Bug Report"
|
||||
icon={<Bug16Regular />}
|
||||
onClick={onClick as any}
|
||||
>
|
||||
File bug
|
||||
</ToolbarButton>
|
||||
></ToolbarButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
|
@ -1,42 +0,0 @@
|
|||
import { css } from "@emotion/react";
|
||||
import { tokens } from "@fluentui/react-components";
|
||||
import type { Diagnostic } from "@typespec/compiler";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
export interface DiagnosticListProps {
|
||||
readonly diagnostics: readonly Diagnostic[];
|
||||
}
|
||||
|
||||
export const DiagnosticList: FunctionComponent<DiagnosticListProps> = ({ diagnostics }) => {
|
||||
if (diagnostics.length === 0) {
|
||||
return <div className="center">No errors</div>;
|
||||
}
|
||||
return (
|
||||
<div css={{ height: "100%", overflow: "auto" }}>
|
||||
{diagnostics.map((x, i) => {
|
||||
return <DiagnosticItem key={i} diagnostic={x} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface DiagnosticItemProps {
|
||||
readonly diagnostic: Diagnostic;
|
||||
}
|
||||
|
||||
export const DiagnosticItem: FunctionComponent<DiagnosticItemProps> = ({ diagnostic }) => {
|
||||
return (
|
||||
<div css={{ display: "flex" }}>
|
||||
<div
|
||||
css={[{ padding: "0 5px" }, diagnostic.severity === "error" ? errorColor : warningColor]}
|
||||
>
|
||||
{diagnostic.severity}
|
||||
</div>
|
||||
<div css={{ padding: "0 5px", color: tokens.colorNeutralForeground2 }}>{diagnostic.code}</div>
|
||||
<div css={{ padding: "0 5px" }}>{diagnostic.message}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const errorColor = css({ color: tokens.colorStatusDangerForeground1 });
|
||||
const warningColor = css({ color: tokens.colorStatusWarningForeground1 });
|
|
@ -0,0 +1,29 @@
|
|||
.list {
|
||||
}
|
||||
|
||||
.item {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.item:hover {
|
||||
background-color: var(--colorNeutralBackground3Hover);
|
||||
}
|
||||
|
||||
.item--error {
|
||||
color: var(--colorStatusDangerForeground1);
|
||||
}
|
||||
|
||||
.item--warning {
|
||||
color: var(--colorStatusWarningForeground1);
|
||||
}
|
||||
|
||||
.item-code {
|
||||
color: var(--colorBrandForeground2);
|
||||
}
|
||||
|
||||
.item-loc {
|
||||
color: var(--colorNeutralForeground3);
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import { mergeClasses } from "@fluentui/react-components";
|
||||
import {
|
||||
getSourceLocation,
|
||||
type Diagnostic,
|
||||
type DiagnosticTarget,
|
||||
type NoTarget,
|
||||
} from "@typespec/compiler";
|
||||
import { FunctionComponent, memo, useCallback } from "react";
|
||||
import style from "./diagnostic-list.module.css";
|
||||
|
||||
export interface DiagnosticListProps {
|
||||
readonly diagnostics: readonly Diagnostic[];
|
||||
readonly onDiagnosticSelected?: (diagnostic: Diagnostic) => void;
|
||||
}
|
||||
|
||||
export const DiagnosticList: FunctionComponent<DiagnosticListProps> = ({
|
||||
diagnostics,
|
||||
onDiagnosticSelected,
|
||||
}) => {
|
||||
if (diagnostics.length === 0) {
|
||||
return <div className={style["list"]}>No errors</div>;
|
||||
}
|
||||
|
||||
const handleItemSelected = useCallback(
|
||||
(diagnostic: Diagnostic) => {
|
||||
onDiagnosticSelected?.(diagnostic);
|
||||
},
|
||||
[onDiagnosticSelected]
|
||||
);
|
||||
return (
|
||||
<div className={style["list"]}>
|
||||
{diagnostics.map((x, i) => {
|
||||
return <DiagnosticItem key={i} diagnostic={x} onItemSelected={handleItemSelected} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface DiagnosticItemProps {
|
||||
readonly diagnostic: Diagnostic;
|
||||
readonly onItemSelected: (diagnostic: Diagnostic) => void;
|
||||
}
|
||||
|
||||
const DiagnosticItem: FunctionComponent<DiagnosticItemProps> = ({ diagnostic, onItemSelected }) => {
|
||||
const handleClick = useCallback(() => {
|
||||
onItemSelected(diagnostic);
|
||||
}, [diagnostic, onItemSelected]);
|
||||
return (
|
||||
<div tabIndex={0} className={style["item"]} onClick={handleClick}>
|
||||
<div
|
||||
className={mergeClasses(
|
||||
(style["item-severity"],
|
||||
style[diagnostic.severity === "error" ? "item--error" : "item--warning"])
|
||||
)}
|
||||
>
|
||||
{diagnostic.severity}
|
||||
</div>
|
||||
<div className={style["item-code"]}>{diagnostic.code}</div>
|
||||
<div className={style["item-message"]}>{diagnostic.message}</div>
|
||||
<div className={style["item-loc"]}>
|
||||
<DiagnosticTargetLink target={diagnostic.target} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DiagnosticTargetLink = memo(({ target }: { target: DiagnosticTarget | typeof NoTarget }) => {
|
||||
if (typeof target === "symbol") {
|
||||
return <span></span>;
|
||||
}
|
||||
const location = getSourceLocation(target);
|
||||
const file = location.file.path === "/test/main.tsp" ? "" : `${location.file.path}:`;
|
||||
const { line, character } = location.file.getLineAndCharacterOfPosition(location.pos);
|
||||
return (
|
||||
<span>
|
||||
{file}
|
||||
{line + 1}:{character + 1}
|
||||
</span>
|
||||
);
|
||||
});
|
|
@ -53,7 +53,7 @@ export const Editor: FunctionComponent<EditorProps> = ({ model, options, actions
|
|||
return (
|
||||
<div
|
||||
className="monaco-editor-container"
|
||||
css={{ width: "100%", height: "100%", overflow: "hidden" }}
|
||||
style={{ width: "100%", height: "100%", overflow: "hidden" }}
|
||||
ref={editorContainerRef}
|
||||
></div>
|
||||
);
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import { css } from "@emotion/react";
|
||||
import { tokens } from "@fluentui/react-components";
|
||||
import type { Diagnostic } from "@typespec/compiler";
|
||||
import { FunctionComponent } from "react";
|
||||
import { DiagnosticList } from "./diagnostic-list.js";
|
||||
|
||||
export interface ErrorTabProps {
|
||||
readonly internalCompilerError?: any;
|
||||
readonly diagnostics?: readonly Diagnostic[];
|
||||
}
|
||||
|
||||
export const ErrorTab: FunctionComponent<ErrorTabProps> = ({
|
||||
internalCompilerError,
|
||||
diagnostics,
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{internalCompilerError && <InternalCompilerError error={internalCompilerError} />}
|
||||
{diagnostics && <DiagnosticList diagnostics={diagnostics} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export interface InternalCompilerErrorProps {
|
||||
readonly error?: any;
|
||||
}
|
||||
|
||||
export const InternalCompilerError: FunctionComponent<InternalCompilerErrorProps> = ({ error }) => {
|
||||
return (
|
||||
<div css={{ CenterStyles }}>
|
||||
<div
|
||||
css={{
|
||||
border: `1px solid ${tokens.colorStatusDangerBorder1}`,
|
||||
padding: "10px",
|
||||
margin: "20px",
|
||||
}}
|
||||
>
|
||||
<h3>Internal Compiler error</h3>
|
||||
<div>File issue at https://github.com/microsoft/typespec</div>
|
||||
<hr />
|
||||
<div>{error.stack}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CenterStyles = css({
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
.file-output {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
.viewer-selector {
|
||||
margin: 0.5rem 1.5rem;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 0;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { css } from "@emotion/react";
|
||||
import { Select, SelectOnChangeData } from "@fluentui/react-components";
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from "react";
|
||||
import { FileOutputViewer } from "./types.js";
|
||||
import { FileOutputViewer } from "../types.js";
|
||||
import style from "./file-output.module.css";
|
||||
|
||||
export interface FileOutputProps {
|
||||
filename: string;
|
||||
|
@ -32,23 +32,18 @@ export const FileOutput: FunctionComponent<FileOutputProps> = ({ filename, conte
|
|||
return viewers.find((x) => x.key === selected)?.render;
|
||||
}, [selected, viewers]);
|
||||
return (
|
||||
<div css={{ width: "100%", height: "100%", overflow: "hidden" }}>
|
||||
<Select value={selected} onChange={handleSelected} css={DropdownStyle}>
|
||||
{viewers.map(({ key, label }) => (
|
||||
<option key={key} value={key}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<div className={style["file-output"]}>
|
||||
<div className={style["viewer-selector"]}>
|
||||
<Select value={selected} onChange={handleSelected}>
|
||||
{viewers.map(({ key, label }) => (
|
||||
<option key={key} value={key}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{selectedRender && selectedRender({ filename, content })}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownStyle = css({
|
||||
margin: "0.5rem 1.5rem",
|
||||
position: "absolute",
|
||||
"z-index": 1,
|
||||
right: 0,
|
||||
});
|
|
@ -16,4 +16,4 @@ export {
|
|||
createReactPlayground,
|
||||
renderReactPlayground,
|
||||
} from "./standalone.js";
|
||||
export * from "./types.js";
|
||||
export type * from "./types.js";
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
import { tokens } from "@fluentui/react-components";
|
||||
import { FunctionComponent, ReactElement } from "react";
|
||||
|
||||
export interface Tab {
|
||||
id: string;
|
||||
name: string | ReactElement<any, any>;
|
||||
align: "left" | "right";
|
||||
}
|
||||
export interface OutputTabsProps {
|
||||
tabs: Tab[];
|
||||
selected: string;
|
||||
onSelect: (file: string) => void;
|
||||
}
|
||||
|
||||
export const OutputTabs: FunctionComponent<OutputTabsProps> = ({ tabs, selected, onSelect }) => {
|
||||
const [leftTabs, rightTabs] = chunk(tabs, (x) => x.align === "left");
|
||||
return (
|
||||
<div css={{ display: "flex", borderBottom: `1px solid ${tokens.colorNeutralStroke1}` }}>
|
||||
{leftTabs.map((tab) => {
|
||||
return (
|
||||
<OutputTab key={tab.id} tab={tab} selected={selected === tab.id} onSelect={onSelect} />
|
||||
);
|
||||
})}
|
||||
<span css={{ flex: 1, borderRight: `1px solid ${tokens.colorNeutralStroke1}` }}></span>
|
||||
{rightTabs.map((tab) => {
|
||||
return (
|
||||
<OutputTab key={tab.id} tab={tab} selected={selected === tab.id} onSelect={onSelect} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export interface OutputTabProps {
|
||||
tab: Tab;
|
||||
selected: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
}
|
||||
export const OutputTab: FunctionComponent<OutputTabProps> = ({ tab, selected, onSelect }) => {
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
css={[
|
||||
{
|
||||
height: "26px",
|
||||
padding: "0 5px",
|
||||
borderRight: `1px solid ${tokens.colorNeutralStroke1}`,
|
||||
borderTop: "none",
|
||||
borderBottom: "none",
|
||||
textDecoration: "none",
|
||||
cursor: "pointer",
|
||||
},
|
||||
selected ? { fontWeight: "bold", backgroundColor: tokens.colorNeutralBackground5 } : {},
|
||||
]}
|
||||
onClick={() => onSelect(tab.id)}
|
||||
>
|
||||
{tab.name}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function chunk<T>(items: T[], condition: (item: T) => boolean) {
|
||||
const match = [];
|
||||
const unMatch = [];
|
||||
for (const item of items) {
|
||||
if (condition(item)) {
|
||||
match.push(item);
|
||||
} else {
|
||||
unMatch.push(item);
|
||||
}
|
||||
}
|
||||
return [match, unMatch];
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
.tabs {
|
||||
border-bottom: 1px solid var(--colorNeutralStroke1);
|
||||
box-shadow: var(--shadow2);
|
||||
}
|
||||
|
||||
.tab-divider {
|
||||
flex: 1;
|
||||
border-right: 1px solid var(--colorNeutralStroke1);
|
||||
}
|
||||
|
||||
.tab {
|
||||
height: 40px;
|
||||
border-right: 1px solid var(--colorNeutralStroke1) !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.tab--selected {
|
||||
background-color: var(--colorNeutralBackground5) !important;
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import {
|
||||
SelectTabData,
|
||||
SelectTabEvent,
|
||||
SelectTabEventHandler,
|
||||
Tab,
|
||||
TabList,
|
||||
TabProps,
|
||||
mergeClasses,
|
||||
} from "@fluentui/react-components";
|
||||
import { FunctionComponent, ReactElement, useCallback } from "react";
|
||||
import style from "./output-tabs.module.css";
|
||||
|
||||
export interface OutputTab {
|
||||
id: string;
|
||||
name: string | ReactElement<any, any>;
|
||||
align: "left" | "right";
|
||||
}
|
||||
export interface OutputTabsProps {
|
||||
tabs: OutputTab[];
|
||||
selected: string;
|
||||
onSelect: (file: string) => void;
|
||||
}
|
||||
|
||||
export const OutputTabs: FunctionComponent<OutputTabsProps> = ({ tabs, selected, onSelect }) => {
|
||||
const [leftTabs, rightTabs] = chunk(tabs, (x) => x.align === "left");
|
||||
|
||||
const onTabSelect: SelectTabEventHandler = useCallback(
|
||||
(event: SelectTabEvent, data: SelectTabData) => {
|
||||
onSelect(data.value as any);
|
||||
},
|
||||
[onSelect]
|
||||
);
|
||||
|
||||
return (
|
||||
<TabList selectedValue={selected} onTabSelect={onTabSelect} className={style["tabs"]}>
|
||||
{leftTabs.map((tab) => {
|
||||
return (
|
||||
<OutputTabEl
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
className={tab.id === selected && style["tab--selected"]}
|
||||
>
|
||||
{tab.name}
|
||||
</OutputTabEl>
|
||||
);
|
||||
})}
|
||||
<div className={style["tab-divider"]}></div>
|
||||
{rightTabs.map((tab) => {
|
||||
return (
|
||||
<OutputTabEl
|
||||
key={tab.id}
|
||||
value={tab.id}
|
||||
className={tab.id === selected && style["tab--selected"]}
|
||||
>
|
||||
{tab.name}
|
||||
</OutputTabEl>
|
||||
);
|
||||
})}
|
||||
</TabList>
|
||||
);
|
||||
};
|
||||
|
||||
const OutputTabEl = (props: TabProps) => {
|
||||
return <Tab {...props} className={mergeClasses(style["tab"], props.className)} />;
|
||||
};
|
||||
|
||||
function chunk<T>(items: T[], condition: (item: T) => boolean) {
|
||||
const match = [];
|
||||
const unMatch = [];
|
||||
for (const item of items) {
|
||||
if (condition(item)) {
|
||||
match.push(item);
|
||||
} else {
|
||||
unMatch.push(item);
|
||||
}
|
||||
}
|
||||
return [match, unMatch];
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
.output-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.output-content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.type-graph-viewer {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
|
@ -1,14 +1,13 @@
|
|||
import { css } from "@emotion/react";
|
||||
import { tokens } from "@fluentui/react-components";
|
||||
import { Diagnostic, Program } from "@typespec/compiler";
|
||||
import { Program } from "@typespec/compiler";
|
||||
import { ColorPalette, ColorProvider, TypeSpecProgramViewer } from "@typespec/html-program-viewer";
|
||||
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { ErrorTab, InternalCompilerError } from "./error-tab.js";
|
||||
import { FileOutput } from "./file-output.js";
|
||||
import { OutputTabs, Tab } from "./output-tabs.js";
|
||||
import { PlaygroundEditorsOptions } from "./playground.js";
|
||||
import { CompilationState, CompileResult, FileOutputViewer, ViewerProps } from "./types.js";
|
||||
import { OutputEditor } from "./typespec-editor.js";
|
||||
import { FileOutput } from "../file-output/file-output.js";
|
||||
import { OutputTab, OutputTabs } from "../output-tabs/output-tabs.js";
|
||||
import { PlaygroundEditorsOptions } from "../playground.js";
|
||||
import { CompilationState, CompileResult, FileOutputViewer, ViewerProps } from "../types.js";
|
||||
import { OutputEditor } from "../typespec-editor.js";
|
||||
import style from "./output-view.module.css";
|
||||
|
||||
export interface OutputViewProps {
|
||||
compilationState: CompilationState | undefined;
|
||||
|
@ -25,7 +24,7 @@ export const OutputView: FunctionComponent<OutputViewProps> = ({
|
|||
return <></>;
|
||||
}
|
||||
if ("internalCompilerError" in compilationState) {
|
||||
return <InternalCompilerError error={compilationState.internalCompilerError} />;
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<OutputViewInternal
|
||||
|
@ -66,41 +65,34 @@ const OutputViewInternal: FunctionComponent<{
|
|||
}
|
||||
|
||||
const diagnostics = program.diagnostics;
|
||||
const tabs: Tab[] = useMemo(() => {
|
||||
const tabs: OutputTab[] = useMemo(() => {
|
||||
return [
|
||||
...outputFiles.map(
|
||||
(x): Tab => ({
|
||||
(x): OutputTab => ({
|
||||
align: "left",
|
||||
name: x,
|
||||
id: x,
|
||||
})
|
||||
),
|
||||
{ id: "type-graph", name: "Type Graph", align: "right" },
|
||||
{
|
||||
id: "errors",
|
||||
name: <ErrorTabLabel diagnostics={diagnostics} />,
|
||||
align: "right",
|
||||
},
|
||||
];
|
||||
}, [outputFiles, diagnostics]);
|
||||
const handleTabSelection = useCallback((tabId: string) => {
|
||||
if (tabId === "type-graph") {
|
||||
setViewSelection({ type: "type-graph" });
|
||||
} else if (tabId === "errors") {
|
||||
setViewSelection({ type: "errors" });
|
||||
} else {
|
||||
void loadOutputFile(tabId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={style["output-view"]}>
|
||||
<OutputTabs
|
||||
tabs={tabs}
|
||||
selected={viewSelection.type === "file" ? viewSelection.filename : viewSelection.type}
|
||||
onSelect={handleTabSelection}
|
||||
/>
|
||||
<div className="output-content" css={{ width: "100%", height: "100%", overflow: "hidden" }}>
|
||||
<div className={style["output-content"]}>
|
||||
<OutputContent
|
||||
viewSelection={viewSelection}
|
||||
editorOptions={editorOptions}
|
||||
|
@ -108,7 +100,7 @@ const OutputViewInternal: FunctionComponent<{
|
|||
viewers={viewers}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -149,51 +141,23 @@ const OutputContent: FunctionComponent<OutputContentProps> = ({
|
|||
viewers={resolvedViewers}
|
||||
/>
|
||||
);
|
||||
case "errors":
|
||||
return <ErrorTab diagnostics={program?.diagnostics} />;
|
||||
default:
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
height: "100%",
|
||||
overflow: "scroll",
|
||||
}}
|
||||
>
|
||||
{program && <TypeGraphViewer program={program} />}
|
||||
</div>
|
||||
);
|
||||
return program && <TypeGraphViewer program={program} />;
|
||||
}
|
||||
};
|
||||
|
||||
type ViewSelection =
|
||||
| { type: "file"; filename: string; content: string }
|
||||
| { type: "type-graph" }
|
||||
| { type: "errors" };
|
||||
|
||||
const ErrorTabLabel: FunctionComponent<{
|
||||
diagnostics?: readonly Diagnostic[];
|
||||
}> = ({ diagnostics }) => {
|
||||
const errorCount = diagnostics ? diagnostics.length : 0;
|
||||
return (
|
||||
<div>Errors {errorCount > 0 ? <span css={ErrorTabCountStyles}>{errorCount}</span> : ""}</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ErrorTabCountStyles = css({
|
||||
backgroundColor: tokens.colorStatusDangerBackground3,
|
||||
color: tokens.colorNeutralForegroundOnBrand,
|
||||
padding: "0 5px",
|
||||
borderRadius: "20px",
|
||||
});
|
||||
type ViewSelection = { type: "file"; filename: string; content: string } | { type: "type-graph" };
|
||||
|
||||
interface TypeGraphViewerProps {
|
||||
program: Program;
|
||||
}
|
||||
const TypeGraphViewer = ({ program }: TypeGraphViewerProps) => {
|
||||
return (
|
||||
<ColorProvider colors={TypeGraphColors}>
|
||||
<TypeSpecProgramViewer program={program} />
|
||||
</ColorProvider>
|
||||
<div className={style["type-graph-viewer"]}>
|
||||
<ColorProvider colors={TypeGraphColors}>
|
||||
<TypeSpecProgramViewer program={program} />
|
||||
</ColorProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
.layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { CompilerOptions } from "@typespec/compiler";
|
||||
import { CompilerOptions, Diagnostic } from "@typespec/compiler";
|
||||
import debounce from "debounce";
|
||||
import { KeyCode, KeyMod, MarkerSeverity, Uri, editor } from "monaco-editor";
|
||||
import {
|
||||
|
@ -11,14 +11,16 @@ import {
|
|||
useState,
|
||||
} from "react";
|
||||
import { CompletionItemTag } from "vscode-languageserver";
|
||||
import { getMarkerLocation } from "../services.js";
|
||||
import { EditorCommandBar } from "../editor-command-bar/editor-command-bar.js";
|
||||
import { getMonacoRange } from "../services.js";
|
||||
import { BrowserHost, PlaygroundSample } from "../types.js";
|
||||
import { PlaygroundContextProvider } from "./context/playground-context.js";
|
||||
import { DefaultFooter } from "./default-footer.js";
|
||||
import { EditorCommandBar } from "./editor-command-bar.js";
|
||||
import { OnMountData, useMonacoModel } from "./editor.js";
|
||||
import { useControllableValue } from "./hooks.js";
|
||||
import { OutputView } from "./output-view.js";
|
||||
import { OutputView } from "./output-view/output-view.js";
|
||||
import style from "./playground.module.css";
|
||||
import { ProblemPane } from "./problem-pane/index.js";
|
||||
import Pane from "./split-pane/pane.js";
|
||||
import { SplitPane } from "./split-pane/split-pane.js";
|
||||
import { CompilationState, FileOutputViewer } from "./types.js";
|
||||
|
@ -133,7 +135,7 @@ export const Playground: FunctionComponent<PlaygroundProps> = (props) => {
|
|||
setCompilationState(state);
|
||||
if ("program" in state) {
|
||||
const markers: editor.IMarkerData[] = state.program.diagnostics.map((diag) => ({
|
||||
...getMarkerLocation(typespecCompiler, diag.target),
|
||||
...getMonacoRange(typespecCompiler, diag.target),
|
||||
message: diag.message,
|
||||
severity: diag.severity === "error" ? MarkerSeverity.Error : MarkerSeverity.Warning,
|
||||
tags: diag.code === "deprecated" ? [CompletionItemTag.Deprecated] : undefined,
|
||||
|
@ -226,49 +228,73 @@ export const Playground: FunctionComponent<PlaygroundProps> = (props) => {
|
|||
editorRef.current = editor;
|
||||
}, []);
|
||||
|
||||
const [verticalPaneSizes, setVerticalPaneSizes] = useState<(string | number | undefined)[]>(
|
||||
verticalPaneSizesConst.collapsed
|
||||
);
|
||||
const toggleProblemPane = useCallback(() => {
|
||||
setVerticalPaneSizes((value) => {
|
||||
return value === verticalPaneSizesConst.collapsed
|
||||
? verticalPaneSizesConst.expanded
|
||||
: verticalPaneSizesConst.collapsed;
|
||||
});
|
||||
}, [setVerticalPaneSizes]);
|
||||
|
||||
const onVerticalPaneSizeChange = useCallback(
|
||||
(sizes: number[]) => {
|
||||
setVerticalPaneSizes(sizes);
|
||||
},
|
||||
[setVerticalPaneSizes]
|
||||
);
|
||||
const handleDiagnosticSelected = useCallback(
|
||||
(diagnostic: Diagnostic) => {
|
||||
editorRef.current?.setSelection(getMonacoRange(host.compiler, diagnostic.target));
|
||||
},
|
||||
[setVerticalPaneSizes]
|
||||
);
|
||||
|
||||
return (
|
||||
<PlaygroundContextProvider value={{ host }}>
|
||||
<div
|
||||
css={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
fontFamily: `"Segoe UI", Tahoma, Geneva, Verdana, sans-serif`,
|
||||
}}
|
||||
>
|
||||
<SplitPane
|
||||
initialSizes={["50%", "50%"]}
|
||||
css={{ gridArea: "typespeceditor", width: "100%", height: "100%", overflow: "hidden" }}
|
||||
>
|
||||
<div className={style["layout"]}>
|
||||
<SplitPane sizes={verticalPaneSizes} onChange={onVerticalPaneSizeChange} split="horizontal">
|
||||
<Pane>
|
||||
<EditorCommandBar
|
||||
host={host}
|
||||
selectedEmitter={selectedEmitter}
|
||||
onSelectedEmitterChange={onSelectedEmitterChange}
|
||||
compilerOptions={compilerOptions}
|
||||
onCompilerOptionsChange={onCompilerOptionsChange}
|
||||
samples={props.samples}
|
||||
selectedSampleName={selectedSampleName}
|
||||
onSelectedSampleNameChange={onSelectedSampleNameChange}
|
||||
saveCode={saveCode}
|
||||
formatCode={formatCode}
|
||||
newIssue={props?.links?.githubIssueUrl ? newIssue : undefined}
|
||||
documentationUrl={props.links?.documentationUrl}
|
||||
/>
|
||||
<TypeSpecEditor
|
||||
model={typespecModel}
|
||||
actions={typespecEditorActions}
|
||||
options={props.editorOptions}
|
||||
onMount={onTypeSpecEditorMount}
|
||||
/>
|
||||
<SplitPane initialSizes={["50%", "50%"]}>
|
||||
<Pane>
|
||||
<EditorCommandBar
|
||||
host={host}
|
||||
selectedEmitter={selectedEmitter}
|
||||
onSelectedEmitterChange={onSelectedEmitterChange}
|
||||
compilerOptions={compilerOptions}
|
||||
onCompilerOptionsChange={onCompilerOptionsChange}
|
||||
samples={props.samples}
|
||||
selectedSampleName={selectedSampleName}
|
||||
onSelectedSampleNameChange={onSelectedSampleNameChange}
|
||||
saveCode={saveCode}
|
||||
formatCode={formatCode}
|
||||
newIssue={props?.links?.githubIssueUrl ? newIssue : undefined}
|
||||
documentationUrl={props.links?.documentationUrl}
|
||||
/>
|
||||
<TypeSpecEditor
|
||||
model={typespecModel}
|
||||
actions={typespecEditorActions}
|
||||
options={props.editorOptions}
|
||||
onMount={onTypeSpecEditorMount}
|
||||
/>
|
||||
</Pane>
|
||||
<Pane>
|
||||
<OutputView
|
||||
compilationState={compilationState}
|
||||
editorOptions={props.editorOptions}
|
||||
viewers={props.emitterViewers?.[selectedEmitter]}
|
||||
/>
|
||||
</Pane>
|
||||
</SplitPane>
|
||||
</Pane>
|
||||
<Pane>
|
||||
<OutputView
|
||||
<Pane minSize={30}>
|
||||
<ProblemPane
|
||||
collapsed={verticalPaneSizes[1] === verticalPaneSizesConst.collapsed[1]}
|
||||
compilationState={compilationState}
|
||||
editorOptions={props.editorOptions}
|
||||
viewers={props.emitterViewers?.[selectedEmitter]}
|
||||
onHeaderClick={toggleProblemPane}
|
||||
onDiagnosticSelected={handleDiagnosticSelected}
|
||||
/>
|
||||
</Pane>
|
||||
</SplitPane>
|
||||
|
@ -278,6 +304,10 @@ export const Playground: FunctionComponent<PlaygroundProps> = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
const verticalPaneSizesConst = {
|
||||
collapsed: [undefined, 30],
|
||||
expanded: [undefined, 200],
|
||||
};
|
||||
const outputDir = "./tsp-output";
|
||||
|
||||
async function compile(
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
.header {
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 20px;
|
||||
justify-content: space-between;
|
||||
box-shadow: var(--shadow2);
|
||||
}
|
||||
|
||||
.header--error {
|
||||
background-color: var(--colorStatusDangerBackground2);
|
||||
color: var(--colorNeutralForeground1);
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
color: var(--colorStatusDangerForeground1);
|
||||
}
|
||||
|
||||
.header--warning {
|
||||
background-color: var(--colorStatusWarningBackground2);
|
||||
color: var(--colorNeutralForeground2);
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
color: var(--colorStatusWarningForeground1);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header-chevron {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
.header-chevron--collapsed {
|
||||
transform: rotate(90deg);
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import { mergeClasses } from "@fluentui/react-components";
|
||||
import { ChevronDown16Regular, ErrorCircle16Filled, Warning16Filled } from "@fluentui/react-icons";
|
||||
import { MouseEventHandler, ReactNode, memo } from "react";
|
||||
import { CompilationState } from "../types.js";
|
||||
import style from "./header.module.css";
|
||||
|
||||
export interface ProblemPaneHeaderProps {
|
||||
compilationState: CompilationState | undefined;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
collaped: boolean;
|
||||
}
|
||||
|
||||
export const ProblemPaneHeader = memo(({ compilationState, ...props }: ProblemPaneHeaderProps) => {
|
||||
const noProblem = (
|
||||
<Container status="none" {...props}>
|
||||
No problems
|
||||
</Container>
|
||||
);
|
||||
if (compilationState === undefined) {
|
||||
return noProblem;
|
||||
}
|
||||
if ("internalCompilerError" in compilationState) {
|
||||
return (
|
||||
<Container status="error" {...props}>
|
||||
<ErrorCircle16Filled /> Internal Compiler Error
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
const diagnostics = compilationState.program.diagnostics;
|
||||
if (diagnostics.length === 0) {
|
||||
return noProblem;
|
||||
}
|
||||
const errors = diagnostics.filter((x) => x.severity === "error");
|
||||
const warnings = diagnostics.filter((x) => x.severity === "warning");
|
||||
return (
|
||||
<Container status={errors.length > 0 ? "error" : "warning"} {...props}>
|
||||
{errors.length > 0 ? (
|
||||
<>
|
||||
<ErrorCircle16Filled className={style["error-icon"]} /> {errors.length} errors
|
||||
</>
|
||||
) : null}
|
||||
{warnings.length > 0 ? (
|
||||
<>
|
||||
<Warning16Filled className={style["warning-icon"]} /> {warnings.length} warnings
|
||||
</>
|
||||
) : null}
|
||||
</Container>
|
||||
);
|
||||
});
|
||||
|
||||
interface ContainerProps {
|
||||
collaped: boolean;
|
||||
onClick?: MouseEventHandler<HTMLDivElement>;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
status: "error" | "warning" | "none";
|
||||
}
|
||||
|
||||
const Container = ({ children, className, status, onClick, collaped }: ContainerProps) => {
|
||||
return (
|
||||
<div
|
||||
tabIndex={onClick === undefined ? undefined : 0}
|
||||
className={mergeClasses(
|
||||
style["header"],
|
||||
status === "error" && style["header--error"],
|
||||
status === "warning" && style["header--warning"],
|
||||
className
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className={style["header-content"]}>{children}</div>
|
||||
<ChevronDown16Regular
|
||||
className={mergeClasses(
|
||||
style["header-chevron"],
|
||||
collaped && style["header-chevron--collapsed"]
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export { ProblemPane } from "./problem-pane.js";
|
|
@ -0,0 +1,18 @@
|
|||
.problem-pane {
|
||||
height: 100%;
|
||||
background-color: var(--colorNeutralBackground3);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.problem-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.no-problems {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.internal-compiler-error {
|
||||
padding: 10px;
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { Diagnostic } from "@typespec/compiler";
|
||||
import { FunctionComponent, MouseEventHandler } from "react";
|
||||
import { DiagnosticList } from "../diagnostic-list/diagnostic-list.js";
|
||||
import { CompilationState } from "../types.js";
|
||||
import { ProblemPaneHeader } from "./header.js";
|
||||
import style from "./problem-pane.module.css";
|
||||
|
||||
export interface ProblemPaneProps {
|
||||
readonly collapsed: boolean;
|
||||
readonly compilationState: CompilationState | undefined;
|
||||
readonly onHeaderClick?: MouseEventHandler<HTMLDivElement>;
|
||||
readonly onDiagnosticSelected?: (diagnostic: Diagnostic) => void;
|
||||
}
|
||||
export const ProblemPane: FunctionComponent<ProblemPaneProps> = ({
|
||||
collapsed,
|
||||
compilationState,
|
||||
onHeaderClick,
|
||||
onDiagnosticSelected,
|
||||
}) => {
|
||||
return (
|
||||
<div className={style["problem-pane"]}>
|
||||
<ProblemPaneHeader
|
||||
compilationState={compilationState}
|
||||
onClick={onHeaderClick}
|
||||
collaped={collapsed}
|
||||
/>
|
||||
<div className={style["problem-content"]}>
|
||||
<ProblemPaneContent
|
||||
compilationState={compilationState}
|
||||
onDiagnosticSelected={onDiagnosticSelected}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface ProblemPaneContentProps {
|
||||
readonly compilationState: CompilationState | undefined;
|
||||
readonly onDiagnosticSelected?: (diagnostic: Diagnostic) => void;
|
||||
}
|
||||
const ProblemPaneContent: FunctionComponent<ProblemPaneContentProps> = ({
|
||||
compilationState,
|
||||
onDiagnosticSelected,
|
||||
}) => {
|
||||
if (compilationState === undefined) {
|
||||
return <></>;
|
||||
}
|
||||
if ("internalCompilerError" in compilationState) {
|
||||
return (
|
||||
<pre className={style["internal-compiler-error"]}>
|
||||
{compilationState.internalCompilerError.stack}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
const diagnostics = compilationState.program.diagnostics;
|
||||
return diagnostics.length === 0 ? (
|
||||
<div className={style["no-problems"]}> No problems</div>
|
||||
) : (
|
||||
<DiagnosticList diagnostics={diagnostics} onDiagnosticSelected={onDiagnosticSelected} />
|
||||
);
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
import {
|
||||
Dialog,
|
||||
DialogBody,
|
||||
DialogContent,
|
||||
DialogSurface,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
ToolbarButton,
|
||||
} from "@fluentui/react-components";
|
||||
import { Settings24Regular } from "@fluentui/react-icons";
|
||||
import type { CompilerOptions } from "@typespec/compiler";
|
||||
import { usePlaygroundContext } from "../context/index.js";
|
||||
import { CompilerSettings } from "./compiler-settings.js";
|
||||
|
||||
export interface CompilerSettingsDialogButtonProps {
|
||||
selectedEmitter: string;
|
||||
compilerOptions: CompilerOptions;
|
||||
onCompilerOptionsChange: (options: CompilerOptions) => void;
|
||||
}
|
||||
|
||||
export const CompilerSettingsDialogButton = ({
|
||||
selectedEmitter,
|
||||
compilerOptions,
|
||||
onCompilerOptionsChange,
|
||||
}: CompilerSettingsDialogButtonProps) => {
|
||||
const { host } = usePlaygroundContext();
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<ToolbarButton icon={<Settings24Regular />} />
|
||||
</DialogTrigger>
|
||||
<DialogSurface>
|
||||
<DialogBody>
|
||||
<DialogTitle>Settings</DialogTitle>
|
||||
<DialogContent>
|
||||
<CompilerSettings
|
||||
host={host}
|
||||
selectedEmitter={selectedEmitter}
|
||||
options={compilerOptions}
|
||||
onOptionsChanged={onCompilerOptionsChange}
|
||||
/>
|
||||
</DialogContent>
|
||||
</DialogBody>
|
||||
</DialogSurface>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
|
@ -1,3 +1,4 @@
|
|||
import { Divider } from "@fluentui/react-components";
|
||||
import { CompilerOptions, LinterRuleSet } from "@typespec/compiler";
|
||||
import { FunctionComponent, useCallback } from "react";
|
||||
import { BrowserHost } from "../../types.js";
|
||||
|
@ -5,12 +6,12 @@ import { EmitterOptions } from "../types.js";
|
|||
import { EmitterOptionsForm } from "./emitter-options-form.js";
|
||||
import { LinterForm } from "./linter-form.js";
|
||||
|
||||
export type CompilerSettingsProps = {
|
||||
host: BrowserHost;
|
||||
selectedEmitter: string;
|
||||
options: CompilerOptions;
|
||||
onOptionsChanged: (options: CompilerOptions) => void;
|
||||
};
|
||||
export interface CompilerSettingsProps {
|
||||
readonly host: BrowserHost;
|
||||
readonly selectedEmitter: string;
|
||||
readonly options: CompilerOptions;
|
||||
readonly onOptionsChanged: (options: CompilerOptions) => void;
|
||||
}
|
||||
|
||||
export const CompilerSettings: FunctionComponent<CompilerSettingsProps> = ({
|
||||
selectedEmitter,
|
||||
|
@ -39,9 +40,9 @@ export const CompilerSettings: FunctionComponent<CompilerSettingsProps> = ({
|
|||
[options]
|
||||
);
|
||||
return (
|
||||
<div css={{ padding: 10 }}>
|
||||
<h2>Settings</h2>
|
||||
<div>
|
||||
<>Emitter: {selectedEmitter}</>
|
||||
<Divider style={{ marginTop: 20 }} />
|
||||
<h3>Options</h3>
|
||||
{library && (
|
||||
<EmitterOptionsForm
|
||||
|
@ -50,6 +51,7 @@ export const CompilerSettings: FunctionComponent<CompilerSettingsProps> = ({
|
|||
optionsChanged={emitterOptionsChanged}
|
||||
/>
|
||||
)}
|
||||
<Divider style={{ marginTop: 20 }} />
|
||||
<h3>Linter</h3>
|
||||
<LinterForm
|
||||
libraries={host.libraries}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.switch :global(label) {
|
||||
padding-left: 0;
|
||||
}
|
|
@ -12,12 +12,14 @@ import {
|
|||
import { FunctionComponent, useCallback, useMemo } from "react";
|
||||
import { PlaygroundTspLibrary } from "../../types.js";
|
||||
import { EmitterOptions } from "../types.js";
|
||||
import style from "./emitter-options-form.module.css";
|
||||
|
||||
export interface EmitterOptionsFormProps {
|
||||
library: PlaygroundTspLibrary;
|
||||
options: EmitterOptions;
|
||||
optionsChanged: (options: EmitterOptions) => void;
|
||||
readonly library: PlaygroundTspLibrary;
|
||||
readonly options: EmitterOptions;
|
||||
readonly optionsChanged: (options: EmitterOptions) => void;
|
||||
}
|
||||
|
||||
export const EmitterOptionsForm: FunctionComponent<EmitterOptionsFormProps> = ({
|
||||
library,
|
||||
options,
|
||||
|
@ -42,10 +44,10 @@ export const EmitterOptionsForm: FunctionComponent<EmitterOptionsFormProps> = ({
|
|||
[options, optionsChanged]
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<div className={style["form"]}>
|
||||
{entries.map(([key, value]) => {
|
||||
return (
|
||||
<div key={key} css={{ margin: "16px 0" }}>
|
||||
<div key={key} className={style["form-item"]}>
|
||||
<JsonSchemaPropertyInput
|
||||
emitterOptions={options[library.name] ?? {}}
|
||||
name={key}
|
||||
|
@ -59,19 +61,19 @@ export const EmitterOptionsForm: FunctionComponent<EmitterOptionsFormProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
type JsonSchemaProperty = {
|
||||
type: "string" | "boolean" | "number";
|
||||
description?: string;
|
||||
enum?: string[];
|
||||
default?: any;
|
||||
};
|
||||
interface JsonSchemaProperty {
|
||||
readonly type: "string" | "boolean" | "number";
|
||||
readonly description?: string;
|
||||
readonly enum?: string[];
|
||||
readonly default?: any;
|
||||
}
|
||||
|
||||
type JsonSchemaPropertyInputProps = {
|
||||
emitterOptions: Record<string, unknown>;
|
||||
name: string;
|
||||
prop: JsonSchemaProperty;
|
||||
onChange: (data: { name: string; value: unknown }) => void;
|
||||
};
|
||||
interface JsonSchemaPropertyInputProps {
|
||||
readonly emitterOptions: Record<string, unknown>;
|
||||
readonly name: string;
|
||||
readonly prop: JsonSchemaProperty;
|
||||
readonly onChange: (data: { name: string; value: unknown }) => void;
|
||||
}
|
||||
|
||||
const JsonSchemaPropertyInput: FunctionComponent<JsonSchemaPropertyInputProps> = ({
|
||||
emitterOptions,
|
||||
|
@ -93,27 +95,31 @@ const JsonSchemaPropertyInput: FunctionComponent<JsonSchemaPropertyInputProps> =
|
|||
|
||||
switch (prop.type) {
|
||||
case "boolean":
|
||||
return <Switch label={prettyName} checked={value} onChange={handleChange} />;
|
||||
return (
|
||||
<Switch
|
||||
className={style["switch"]}
|
||||
label={prettyName}
|
||||
labelPosition="above"
|
||||
checked={value}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
case "string":
|
||||
default:
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Label htmlFor={inputId} title={name}>
|
||||
{prettyName}
|
||||
</Label>
|
||||
</div>
|
||||
<div>
|
||||
{prop.enum ? (
|
||||
<RadioGroup layout="horizontal" id={inputId} value={value} onChange={handleChange}>
|
||||
{prop.enum.map((x) => (
|
||||
<Radio key={x} value={x} label={x} />
|
||||
))}
|
||||
</RadioGroup>
|
||||
) : (
|
||||
<Input id={inputId} value={value} onChange={handleChange} />
|
||||
)}
|
||||
</div>
|
||||
<div className={style["item"]}>
|
||||
<Label htmlFor={inputId} title={name}>
|
||||
{prettyName}
|
||||
</Label>
|
||||
{prop.enum ? (
|
||||
<RadioGroup layout="horizontal" id={inputId} value={value} onChange={handleChange}>
|
||||
{prop.enum.map((x) => (
|
||||
<Radio key={x} value={x} label={x} />
|
||||
))}
|
||||
</RadioGroup>
|
||||
) : (
|
||||
<Input id={inputId} value={value} onChange={handleChange} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -49,6 +49,24 @@
|
|||
background-color: var(--colorNeutralStroke1Pressed);
|
||||
}
|
||||
|
||||
.sash-vertical .sash-content {
|
||||
width: 1px;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.sash-horizontal .sash-content {
|
||||
height: 1px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.sash-vertical .sash-content-dragging {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sash-horizontal .sash-content-dragging {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pane {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
|
|
|
@ -19,8 +19,8 @@ export interface SplitPaneProps {
|
|||
children: JSX.Element[];
|
||||
allowResize?: boolean;
|
||||
split?: "vertical" | "horizontal";
|
||||
initialSizes: (string | number)[];
|
||||
sizes?: (string | number)[];
|
||||
initialSizes?: (string | number)[];
|
||||
sizes?: (string | number | undefined)[];
|
||||
sashRender?: (index: number, active: boolean) => React.ReactNode;
|
||||
onChange?: (sizes: number[]) => void;
|
||||
onDragStart?: (e: MouseEvent) => void;
|
||||
|
@ -56,7 +56,7 @@ export const SplitPane: FunctionComponent<SplitPaneProps> = ({
|
|||
onDragEnd = () => null,
|
||||
...others
|
||||
}: SplitPaneProps) => {
|
||||
const [resolvedPropSize, updateSizes] = useControllableValue<(string | number)[]>(
|
||||
const [resolvedPropSize, updateSizes] = useControllableValue<(string | number | undefined)[]>(
|
||||
propSizes,
|
||||
defaultSizes,
|
||||
onChange as any
|
||||
|
@ -151,7 +151,9 @@ export const SplitPane: FunctionComponent<SplitPaneProps> = ({
|
|||
);
|
||||
|
||||
const resetPosition = useCallback(() => {
|
||||
updateSizes(defaultSizes);
|
||||
if (defaultSizes) {
|
||||
updateSizes(defaultSizes);
|
||||
}
|
||||
}, [defaultSizes, updateSizes]);
|
||||
|
||||
const dragEnd = useCallback(
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import { FunctionComponent, useEffect, useState } from "react";
|
||||
|
||||
export interface SwaggerUIProps {
|
||||
spec: string;
|
||||
}
|
||||
|
||||
export const SwaggerUI: FunctionComponent<SwaggerUIProps> = (props) => {
|
||||
const [swaggerUI, setSwaggerUILib] = useState<
|
||||
{ swaggerUIComp: typeof import("swagger-ui-react").default } | undefined
|
||||
>(undefined);
|
||||
useEffect(() => {
|
||||
void import("swagger-ui-react").then((lib) => {
|
||||
setSwaggerUILib({ swaggerUIComp: lib.default as any });
|
||||
});
|
||||
}, [setSwaggerUILib]);
|
||||
|
||||
if (swaggerUI === undefined) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
css={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<swaggerUI.swaggerUIComp spec={props.spec} />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import "swagger-ui-react/swagger-ui.css";
|
||||
import { FileOutputViewer, ViewerProps } from "../types.js";
|
||||
import { SwaggerUI } from "./swagger-ui.js";
|
||||
import { FileOutputViewer, ViewerProps } from "./types.js";
|
||||
|
||||
export const SwaggerUIViewer: FileOutputViewer = {
|
||||
key: "swaggerUI",
|
|
@ -0,0 +1,5 @@
|
|||
.swagger-ui-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { FunctionComponent, Suspense, lazy } from "react";
|
||||
import style from "./swagger-ui.module.css";
|
||||
|
||||
export interface SwaggerUIProps {
|
||||
readonly spec: string;
|
||||
}
|
||||
|
||||
const LazySwaggerUI = lazy(() => import("swagger-ui-react") as any);
|
||||
|
||||
export const SwaggerUI: FunctionComponent<SwaggerUIProps> = (props) => {
|
||||
return (
|
||||
<Suspense fallback={<div />}>
|
||||
<div className={style["swagger-ui-container"]}>
|
||||
<LazySwaggerUI spec={props.spec} />
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
|
@ -5,7 +5,6 @@ import {
|
|||
TypeSpecLanguageConfiguration,
|
||||
} from "@typespec/compiler";
|
||||
import * as monaco from "monaco-editor";
|
||||
import { editor } from "monaco-editor";
|
||||
import * as lsp from "vscode-languageserver";
|
||||
import { DocumentHighlightKind, FormattingOptions } from "vscode-languageserver";
|
||||
import { TextDocument } from "vscode-languageserver-textdocument";
|
||||
|
@ -360,10 +359,10 @@ export async function registerMonacoLanguage(host: BrowserHost) {
|
|||
});
|
||||
}
|
||||
|
||||
export function getMarkerLocation(
|
||||
export function getMonacoRange(
|
||||
typespecCompiler: typeof import("@typespec/compiler"),
|
||||
target: DiagnosticTarget | typeof NoTarget
|
||||
): Pick<editor.IMarkerData, "startLineNumber" | "startColumn" | "endLineNumber" | "endColumn"> {
|
||||
): monaco.IRange {
|
||||
const loc = typespecCompiler.getSourceLocation(target);
|
||||
if (loc === undefined || loc.file.path !== "/test/main.tsp") {
|
||||
return {
|
||||
|
|
|
@ -28,12 +28,7 @@ export function definePlaygroundViteConfig(config: PlaygroundUserConfig): UserCo
|
|||
exclude: ["swagger-ui"],
|
||||
},
|
||||
plugins: [
|
||||
react({
|
||||
jsxImportSource: "@emotion/react",
|
||||
babel: {
|
||||
plugins: ["@emotion/babel-plugin"],
|
||||
},
|
||||
}),
|
||||
react({}),
|
||||
playgroundManifestPlugin(config),
|
||||
!config.skipBundleLibraries
|
||||
? typespecBundlePlugin({
|
||||
|
|
|
@ -11,9 +11,7 @@
|
|||
"rootDir": ".",
|
||||
"skipLibCheck": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@emotion/react",
|
||||
"lib": ["DOM"],
|
||||
"types": ["@emotion/react"]
|
||||
"lib": ["DOM"]
|
||||
},
|
||||
"include": [
|
||||
"rollup.config.ts",
|
||||
|
|
|
@ -21,14 +21,7 @@ const config = defineConfig({
|
|||
optimizeDeps: {
|
||||
exclude: ["swagger-ui"],
|
||||
},
|
||||
plugins: [
|
||||
react({
|
||||
jsxImportSource: "@emotion/react",
|
||||
babel: {
|
||||
plugins: ["@emotion/babel-plugin"],
|
||||
},
|
||||
}),
|
||||
],
|
||||
plugins: [react({})],
|
||||
server: {
|
||||
fs: {
|
||||
strict: false,
|
||||
|
|
Загрузка…
Ссылка в новой задаче