Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function parseWorkflow(text: string): WorkflowParseResult {
}

if (raw == null || typeof raw !== "object") {
return { model: null, errors: [new Error("Not a valid workflow object")] };
return { model: null, errors: [new Error("Not a valid workflow")] };
}

const model = new Classes.Workflow(raw) as Specification.Workflow;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
import * as React from "react";
import { Diagram, DiagramRef } from "../react-flow/diagram/Diagram";
import { DiagramEditorContextProvider } from "../store/DiagramEditorContextProvider";
import { I18nProvider, useI18n, detectLocale } from "@serverlessworkflow/i18n";
import { I18nProvider, detectLocale } from "@serverlessworkflow/i18n";
import { dictionaries } from "../i18n/locales";
import { useDiagramEditorContext } from "../store/DiagramEditorContext";
import { ParsingErrorPage } from "./error-pages/ParsingErrorPage";
/**
* DiagramEditor component API
*/
Expand All @@ -33,19 +35,28 @@ export type DiagramEditorProps = {
ref?: React.Ref<DiagramEditorRef>;
};

const Content = () => {
const { t } = useI18n();
return <p>{t("helloMessage")}</p>;
const DiagramEditorContent = ({
diagramRef,
diagramDivRef,
}: {
diagramRef: React.RefObject<DiagramRef | null>;
diagramDivRef: React.RefObject<HTMLDivElement | null>;
}) => {
const { model } = useDiagramEditorContext();
return model === null ? (
<ParsingErrorPage />
) : (
<Diagram ref={diagramRef} divRef={diagramDivRef} />
);
};

export const DiagramEditor = (props: DiagramEditorProps) => {
// TODO: i18n
// TODO: store, context
// TODO: ErrorBoundary / fallback

// Refs
const diagramDivRef = React.useRef<HTMLDivElement | null>(null);
const diagramRef = React.useRef<DiagramRef | null>(null);

const locale = React.useMemo(() => {
const supportedLocales = Object.keys(dictionaries);
return props.locale ?? detectLocale(supportedLocales);
Expand All @@ -64,10 +75,13 @@ export const DiagramEditor = (props: DiagramEditorProps) => {

return (
<>
<DiagramEditorContextProvider content={props.content} isReadOnly={props.isReadOnly} locale={locale}>
<DiagramEditorContextProvider
content={props.content}
isReadOnly={props.isReadOnly}
locale={locale}
>
<I18nProvider locale={locale} dictionaries={dictionaries}>
<Content />
<Diagram ref={diagramRef} divRef={diagramDivRef} />
<DiagramEditorContent diagramRef={diagramRef} diagramDivRef={diagramDivRef} />
</I18nProvider>
</DiagramEditorContextProvider>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

type ErrorPageProps = {
title: string;
message?: string | undefined;
snippet?: string | undefined;
};

export const ErrorPage = ({ title, message, snippet }: ErrorPageProps) => {
// TODO: Apply styling later
return (
<div style={{ padding: "1rem" }}>
<h2>{title}</h2>
{message ? <p>{message}</p> : null}
{snippet ? (
<pre>
<code>{snippet}</code>
</pre>
) : null}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { useI18n } from "@serverlessworkflow/i18n";
import { useDiagramEditorContext } from "../../store/DiagramEditorContext";
import { ErrorPage } from "./ErrorPage";

type YAMLExceptionLike = Error & {
reason?: string;
mark?: { line: number; column: number; snippet?: string };
};

const isYAMLException = (err: Error): err is YAMLExceptionLike => err.name === "YAMLException";

export const ParsingErrorPage = () => {
const { errors } = useDiagramEditorContext();
const { t } = useI18n();
// YAML parsing errors the only errors we expect for now so we will just take the first/only error
const err = errors[0];

if (err && isYAMLException(err)) {
return (
<ErrorPage
title={t("workflowError.parsing.title")}
message={err.reason}
snippet={err.mark?.snippet}
/>
);
}

// Fallback (covers both no errors and non-YAML errors)
return <ErrorPage title={t("workflowError.title")} message={t("workflowError.default")} />;
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
*/

export const en = {
helloMessage: "Hello from Serverless Workflow Specification Editor!",
"workflowError.title": "Workflow Error",
"workflowError.default": "There was an error loading the workflow.",
"workflowError.parsing.title": "Parsing Error",
} as const;

export type TranslationKeys = keyof typeof en;
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@
*/

import { en } from "./en";
import { fr } from "./fr";

export const dictionaries = {
en,
fr,
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";

import { DiagramEditor } from "./DiagramEditor";
import { BASIC_VALID_WORKFLOW_YAML } from "../tests/fixtures/workflows";

const meta = {
title: "Example/DiagramEditor",
Expand All @@ -37,6 +38,6 @@ export const Component: Story = {
args: {
isReadOnly: true,
locale: "en",
content: "", // TODO: Replace with a sample workflow YAML once diagram renders from model
content: BASIC_VALID_WORKFLOW_YAML, // TODO: Add better workflow sample when removing hardcoded nodes and edges in Diagram component
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { Meta, StoryObj } from "@storybook/react-vite";
import { ErrorPage } from "../src/diagram-editor/error-pages/ErrorPage";

const meta = {
title: "Example/ErrorPage",
component: ErrorPage,
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ["autodocs"],
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
layout: "fullscreen",
},
args: {},
} satisfies Meta<typeof ErrorPage>;

export default meta;
type Story = StoryObj<typeof meta>;

export const TitleOnly: Story = {
args: {
title: "Something went wrong",
},
};

export const WithMessage: Story = {
args: {
title: "Something went wrong",
message: "An unexpected error occurred while processing your request.",
},
};

export const WithSnippet: Story = {
args: {
title: "YAML Syntax Error",
snippet: `tasks:
- myTask
call: http
method: get,
endpoint: "http://example.com/api"
`,
},
};

export const WithMessageAndSnippet: Story = {
args: {
title: "YAML Syntax Error",
message: "Bad indentation",
snippet: `tasks:
- myTask
call: http
method: get,
endpoint: "http://example.com/api"
`,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe("parseWorkflow", () => {
])("returns null model with error for $description", ({ input }) => {
const result = parseWorkflow(input);
expect(result.model).toBeNull();
expect(result.errors[0].message).toBe("Not a valid workflow object");
expect(result.errors[0].message).toBe("Not a valid workflow");
});

it("returns null model with errors for unparseable text", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { render, screen } from "@testing-library/react";
import { composeStories } from "@storybook/react-vite";
import * as stories from "../../stories/DiagramEditor.stories";
import { vi, test, expect, afterEach, describe } from "vitest";
import { vi, expect, afterEach, describe, it } from "vitest";
import { BASIC_VALID_WORKFLOW_YAML } from "../fixtures/workflows";

// Composes all stories in the file
Expand All @@ -28,7 +28,7 @@ describe("Story - DiagramEditor component", () => {
vi.restoreAllMocks();
});

test("Renders react flow Diagram component", async () => {
it("Renders react flow Diagram component", async () => {
const locale = "en";
const isReadOnly = true;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2021-Present The Serverless Workflow Specification Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { render, screen } from "@testing-library/react";
import { composeStories } from "@storybook/react-vite";
import * as stories from "../../../stories/ErrorPage.stories";
import { expect, describe, it } from "vitest";

const { TitleOnly, WithMessage, WithSnippet, WithMessageAndSnippet } = composeStories(stories);

describe("Story - ErrorPage component", () => {
it("Renders title only", async () => {
render(<TitleOnly />);
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
});

it("Renders with message", async () => {
render(<WithMessage />);
expect(screen.getByText("Something went wrong")).toBeInTheDocument();
expect(screen.getByText("An unexpected error occurred while processing your request.")).toBeInTheDocument();
});

it("Renders with Snippet", async () => {
render(<WithSnippet />);
expect(screen.getByText("YAML Syntax Error")).toBeInTheDocument();
expect(screen.getByText(/call: http/)).toBeInTheDocument();
});

it("Renders with message and snippet", async () => {
render(<WithMessageAndSnippet />);
expect(screen.getByText("YAML Syntax Error")).toBeInTheDocument();
expect(screen.getByText("Bad indentation")).toBeInTheDocument();
expect(screen.getByText(/call: http/)).toBeInTheDocument();
});
});
Loading
Loading