diff --git a/apps/tour/public/aws/aws_iam_role.svg b/apps/tour/public/aws/aws_iam_role.svg new file mode 100644 index 00000000..79330f47 --- /dev/null +++ b/apps/tour/public/aws/aws_iam_role.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/tour/public/aws/aws_iam_role_policy.svg b/apps/tour/public/aws/aws_iam_role_policy.svg new file mode 100644 index 00000000..11c091ba --- /dev/null +++ b/apps/tour/public/aws/aws_iam_role_policy.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/tour/public/aws/aws_iam_role_policy_attachment.svg b/apps/tour/public/aws/aws_iam_role_policy_attachment.svg new file mode 100644 index 00000000..11c091ba --- /dev/null +++ b/apps/tour/public/aws/aws_iam_role_policy_attachment.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/tour/public/aws/aws_lambda_function.svg b/apps/tour/public/aws/aws_lambda_function.svg new file mode 100644 index 00000000..b025b9da --- /dev/null +++ b/apps/tour/public/aws/aws_lambda_function.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/tour/public/aws/aws_lambda_permission.svg b/apps/tour/public/aws/aws_lambda_permission.svg new file mode 100644 index 00000000..b025b9da --- /dev/null +++ b/apps/tour/public/aws/aws_lambda_permission.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/tour/public/aws/aws_s3_bucket.svg b/apps/tour/public/aws/aws_s3_bucket.svg new file mode 100644 index 00000000..0856fd63 --- /dev/null +++ b/apps/tour/public/aws/aws_s3_bucket.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/tour/public/aws/aws_s3_object.svg b/apps/tour/public/aws/aws_s3_object.svg new file mode 100644 index 00000000..f51e0059 --- /dev/null +++ b/apps/tour/public/aws/aws_s3_object.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/tour/public/aws/aws_sns_topic.svg b/apps/tour/public/aws/aws_sns_topic.svg new file mode 100644 index 00000000..da75e882 --- /dev/null +++ b/apps/tour/public/aws/aws_sns_topic.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/tour/public/aws/aws_sqs_queue.svg b/apps/tour/public/aws/aws_sqs_queue.svg new file mode 100644 index 00000000..a28e479c --- /dev/null +++ b/apps/tour/public/aws/aws_sqs_queue.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/tour/src/Editor.tsx b/apps/tour/src/Editor.tsx index fff7d91d..5cff8c09 100644 --- a/apps/tour/src/Editor.tsx +++ b/apps/tour/src/Editor.tsx @@ -25,7 +25,7 @@ import { WebContainer } from '@webcontainer/api'; import ReactMarkdown from 'react-markdown' import { Loading } from '@wing-playground/shared/src/Loading'; -import { Compiler, Target } from '@wing-playground/shared/src/compiler/compiler'; +import { Compiler, Target, CompilationItem } from '@wing-playground/shared/src/compiler/compiler'; import { CompilationRequest } from '@wing-playground/shared/src/compiler/request'; import { useExamples, Example } from '@wing-playground/shared/src/use-examples.js'; import { tutorials } from './tutorials/index.js'; @@ -38,9 +38,13 @@ import {useEditor} from "@wing-playground/shared/src/editor/use-editor"; import {installDependencies, ConsoleLayouts} from "@wing-playground/shared/src/containers"; import {WelcomeModal} from "./WelcomeModal"; import {CongratsModal} from "./CongratsModal"; + import {SimulatorTarget} from "@wing-playground/shared/src/SimulatorTarget"; + import {TargetsView, TargetView} from "./TargetsView"; import {PanelHeader} from "@wing-playground/shared/src/PanelHeader"; +import { TfAwsTarget } from './TfAwsTarget.js'; +import { debounce } from 'lodash'; const wingPackageJson = await import("winglang/package.json?raw").then( (i) => JSON.parse(i.default) @@ -92,7 +96,19 @@ export const ReactMonacoEditor: React.FC = ({ version: wingPackageJson.version }); } - const {evaluateCode, editorWillMount, editorDidMount } = useEditor({ + + const [targets, setTargets] = useState(["simulator"]); + const compilerTargets = useMemo(() => { + return targets.filter((target) => { + return target !== "simulator"; + }).map((target) => target as Target); + }, [targets]); + + const { + evaluateCode, + editorWillMount, + editorDidMount, + } = useEditor({ editorRef, onLoadingStatusChange: setLoadingStatus, onLspError, @@ -100,10 +116,14 @@ export const ReactMonacoEditor: React.FC = ({ languageContext, code: tutorials[0].code, compiler, + targets: compilerTargets, editorOptions, shouldInitContainer: true, }); + const [isCompiling, setIsCompiling] = useState(false); + const [compilationItems, setCompilationItems] = useState([]); + useEffect(() => { if (ref.current != null) { return () => { @@ -165,45 +185,107 @@ export const ReactMonacoEditor: React.FC = ({ }, [currentStep, steps]); useEffect(() => { - if (!currentStep) { - return; - } + if (!currentStep) { + return; + } - editorRef.current?.setValue(currentStep.code); + editorRef.current?.setValue(currentStep.code); - analytics.track(`tutorial: step: ${currentStepId}: changed`, { - step: currentStep - }) + analytics.track(`tutorial: step: ${currentStepId}: changed`, { + step: currentStep + }) + setTargets(currentStep.targets); + setCurrentTargetId(targetViews[0]?.title); }, [currentStep]); const downloadCompiledCode = async (target: Target) => { - setDownloadInProgress(true); - const result = await compiler.compile(new CompilationRequest(editorRef.current?.getValue()!, target)); - if (result.error) { - console.error('compilation failed', result.error.stderr); - return; - } - console.log("download compile code", result); - const zipBlob = new Blob([new Uint8Array(result.zip.toBuffer())]); - const url = window.URL.createObjectURL(zipBlob); - const zipDownload = document.createElement("a"); - zipDownload.href = url; - zipDownload.download = "hello.tfaws.zip"; - document.body.appendChild(zipDownload); - zipDownload.click(); - setDownloadInProgress(false); + setDownloadInProgress(true); + const result = await compiler.compile(new CompilationRequest(editorRef.current?.getValue()!, target)); + if (result.error) { + console.error('compilation failed', result.error.stderr); + return; + } + console.log("download compile code", result); + const zipBlob = new Blob([new Uint8Array(result.zip.toBuffer())]); + const url = window.URL.createObjectURL(zipBlob); + const zipDownload = document.createElement("a"); + zipDownload.href = url; + zipDownload.download = "hello.tfaws.zip"; + document.body.appendChild(zipDownload); + zipDownload.click(); + setDownloadInProgress(false); }; + const retrieveCompilationFiles = useCallback(debounce(async (value: string, target: Target) => { + setIsCompiling(true); + const request = new CompilationRequest(value, target); + const result = await compiler.compile(request); + if (result.error) { + console.error('compilation failed', result.error.stderr); + setIsCompiling(false); + return; + } + setCompilationItems(result.files); + setIsCompiling(false); + }, 1000), [compiler]); + + const [showWelcomeModal, setShowWelcomeModal] = useState(true); const [showFinishModal, setShowFinishModal] = useState(false); const simulatorTarget: TargetView = useMemo(() => { - return { - title: "Wing Simulator", - Target: () => - } + return { + id: "simulator", + title: "Wing Simulator", + Target: () => + } }, [iframSrc, refIframe]); + const tfAwsTarget: TargetView = useMemo(() => { + return { + id: Target.TFAWS, + title: "AWS/TERRAFORM", + Target: () => downloadCompiledCode(Target.TFAWS)} + disabled={downloadInProgress} + /> + } + }, [isCompiling, downloadInProgress, downloadCompiledCode, compilationItems]); + + const targetViews: TargetView[] = useMemo(() => { + const views: TargetView[] = []; + + if (!targets || targets.length === 0) { + return [simulatorTarget]; + } + + targets.forEach(target => { + if (target === "simulator") { + views.push(simulatorTarget); + } + if (target === Target.TFAWS) { + views.push(tfAwsTarget); + } + }); + return views; + }, [targets, simulatorTarget, tfAwsTarget]); + + const [currentTargetId, setCurrentTargetId] = useState(targetViews[0]?.id); + + useEffect(() => { + setCurrentTargetId(targetViews[0]?.id); + }, [targetViews.length]); + + useEffect(() => { + setCompilationItems([]); + if (targets.includes(Target.TFAWS)) { + const value = editorRef.current?.getValue(); + retrieveCompilationFiles(value || "", Target.TFAWS); + } + }, [targets, editorRef.current?.getValue()]); + return ( <>
@@ -252,11 +334,6 @@ export const ReactMonacoEditor: React.FC = ({ }
- {isLastStep && - - } {!isFirstStep &&
- {loadingStatus != LoadingStatus.Completed ? - : - - } + {loadingStatus != LoadingStatus.Completed && + + } + {loadingStatus == LoadingStatus.Completed && ( + + )}
diff --git a/apps/tour/src/TargetsView.tsx b/apps/tour/src/TargetsView.tsx index 9a24e575..7c0e8cc2 100644 --- a/apps/tour/src/TargetsView.tsx +++ b/apps/tour/src/TargetsView.tsx @@ -1,24 +1,54 @@ -import {SimulatorTarget} from "@wing-playground/shared/src/SimulatorTarget"; -import React from "react"; -import {PanelHeader} from "@wing-playground/shared/src/PanelHeader"; +import {FC, useEffect, useMemo, useState } from "react"; +import { Tab, Tabs } from "@wing-playground/shared/src/Tabs"; +import classNames from "classnames"; export interface TargetView { - title: string; - Target: React.FC; + id: string; + title: string; + Target: FC; } export interface TargetsViewProps { - targets: TargetView[]; - currentTarget?: TargetView; - setCurrentTarget?: (target: TargetView) => void; + targets: TargetView[]; + currentTargetId?: string; + setCurrentTargetId?: (targetId: string) => void; +} +export const TargetsView = ({targets, setCurrentTargetId, currentTargetId}: TargetsViewProps) => { + + const tabs = useMemo(() => { + const tabs: Tab[] = []; + targets.forEach((target) => { + tabs.push({ + id: target.id, + name: target.title, + panel: , + }); + }); + return tabs; + }, [targets]); + + useEffect(() => { + if (targets.find((target) => target.id === currentTargetId)) { + return; + } + setCurrentTargetId?.(targets[0].id); + }, [currentTargetId, targets, setCurrentTargetId]); + + return ( + { + const target = targets.find((target) => target.id === tabId); + if (!target || !setCurrentTargetId) { + return; + } + setCurrentTargetId(target.id); + }} + /> + ); } -export const TargetsView = ({targets, setCurrentTarget, currentTarget}: TargetsViewProps) => { - return ( -
- {targets.map((target, index) => ( -
- {target.title} - -
))} -
- ); -} \ No newline at end of file diff --git a/apps/tour/src/TfAwsTarget.tsx b/apps/tour/src/TfAwsTarget.tsx new file mode 100644 index 00000000..619c9df6 --- /dev/null +++ b/apps/tour/src/TfAwsTarget.tsx @@ -0,0 +1,296 @@ +import {useEffect, useMemo, useRef, useState} from "react"; +import Editor from "@monaco-editor/react"; + +import "monaco-editor/esm/vs/editor/editor.all.js"; + +// support all editor features +import "monaco-editor/esm/vs/editor/standalone/browser/accessibilityHelp/accessibilityHelp.js"; +import "monaco-editor/esm/vs/editor/standalone/browser/inspectTokens/inspectTokens.js"; +import "monaco-editor/esm/vs/editor/standalone/browser/iPadShowKeyboard/iPadShowKeyboard.js"; +import "monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess.js"; +import "monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.js"; +import "monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneGotoSymbolQuickAccess.js"; +import "monaco-editor/esm/vs/editor/standalone/browser/quickAccess/standaloneCommandsQuickAccess.js"; +import "monaco-editor/esm/vs/editor/standalone/browser/quickInput/standaloneQuickInputService.js"; +import "monaco-editor/esm/vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch.js"; +import "monaco-editor/esm/vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast.js"; + +import * as monaco from 'monaco-editor'; +import classNames from "classnames"; +import { CompilationItem } from "@wing-playground/shared/src/compiler/compiler"; +import { Loading } from "@wing-playground/shared/src/Loading"; +import { ArrowDownTrayIcon } from "@heroicons/react/24/solid"; + +const getResourceName = (type: string) => { + switch (type) { + case "aws_sqs_queue": + return "SQS"; + case "aws_s3_bucket": + return "S3"; + case "aws_s3_object": + return "S3 Object"; + case "aws_lambda_function": + return "Lambda"; + case "aws_iam_role": + return "IAM Role"; + case "aws_iam_role_policy": + return "IAM Policy"; + case "aws_sns_topic": + return "SNS"; + default: + return type.split("_").slice(1).join(" ").toLowerCase(); + } +} + +const ResourceIcon = ({type}: {type: string}) => { + const resources = [ + "aws_sqs_queue", + "aws_s3_bucket", + "aws_s3_object", + "aws_lambda_function", + "aws_lambda_permission", + "aws_iam_role", + "aws_iam_role_policy", + "aws_iam_role_policy_attachment", + "aws_sns_topic", + ]; + + if (!resources.includes(type)) { + return null; + } + return +} + +interface Item { + id: string; + name: string; + description?: string; + type?: string; + contents: string; +} + +const FileRow = ({title, description, icon, selected, onClick}: { + title: string, + description?: string, + icon?: React.ReactNode, + selected: boolean, + onClick: () => void +}) => { + return ( +
+ +
+ ) +} + +const ItemsList = ({ + title, + items, + selectedItem, + loading, + placeholder, + onClick, + actions, +}:{ + title: string; + items: Item[]; + selectedItem?: Item; + loading: boolean; + placeholder?: string; + onClick: (item: Item) => void; + actions?: React.ReactNode; +}) => { + return ( + <> +
+
+
+ {title} + ({items?.length || 0}) +
+
+ {actions} +
+
+
+
+
+
+ {items.length === 0 && !loading && ( +
+ {placeholder} +
+ )} + {items.map((item) => { + return ( + } + selected={selectedItem?.id === item.id} + onClick={() => onClick(item)} + /> + ) + })} +
+
+
+ + ) +}; + +export interface TfAwsTargetProps { + files?: CompilationItem[]; + downloadCompiledCode?: () => void; + loading?: boolean; + disabled?: boolean; +} + +export const TfAwsTarget = ({ + files, + downloadCompiledCode, + loading = false, + disabled = false +}: TfAwsTargetProps) => { + const compileEditorRef = useRef(); + const [selectedItem, setSelectedItem] = useState(); + + const resources: Item[] = useMemo(() => { + if (!files) { + return []; + } + + const tfFile = files?.find((file) => file.name === "main.tf.json")?.contents; + if (!tfFile) { + return []; + } + try { + const json = JSON.parse(tfFile); + if (!json.resource) { + return []; + } + const resources: Item[] = []; + for (const [key, value] of Object.entries(json.resource)) { + for (const [key2, value2] of Object.entries(value as object)) { + const path = value2["//"]["metadata"]["path"] || key2; + const resourceName = path.split("/").slice(-2, -1)[0]; + if (value2.policy) { + value2.policy = JSON.parse(value2.policy); + } + if (value2.assume_role_policy) { + value2.assume_role_policy = JSON.parse(value2.assume_role_policy); + } + resources.push({ + id: path, + name: getResourceName(key), + description: resourceName, + type: key, + contents: JSON.stringify(value2, null, 2) + }); + } + } + return resources; + } catch (e) { + return []; + } + }, [files]); + + const assets: Item[] = useMemo(() => { + if (!files) { + return []; + } + const newAssets = files.filter((f) => f.name.startsWith(".wing/clients/")).map((file, index) => { + return { + name: `inflight${index + 1}.js`, + contents: file.contents, + } + }); + return newAssets.map((asset) => { + return { + id: asset.name, + name: asset.name, + contents: asset.contents, + } + }); + }, [files]); + + const options: monaco.editor.IStandaloneEditorConstructionOptions = { + minimap: { enabled: false }, + }; + + const compileEditorDidMount = async (editor: any, monaco: any) => { + compileEditorRef.current = editor + } + + useEffect(() => { + setSelectedItem(resources[0] || assets[0]); + }, [assets, resources]); + + + return ( +
+ {loading && ( +
+ +
+ )} +
+ + + + // + // + // } + items={assets} + selectedItem={selectedItem} + loading={loading} + placeholder="No assets found" + onClick={setSelectedItem} + /> +
+ +
+ +
+
+ ) +} diff --git a/apps/tour/src/WelcomeModal.tsx b/apps/tour/src/WelcomeModal.tsx index 85507234..02ffcd8f 100644 --- a/apps/tour/src/WelcomeModal.tsx +++ b/apps/tour/src/WelcomeModal.tsx @@ -8,18 +8,18 @@ export const WelcomeModal = ({visible, onClose}: {visible: boolean, onClose: ()

Welcome to the Winglang Tutorial!

- Let's quickly build a smart queue that prints its messages and stores the latest one in a bucket. + Let's quickly learn about Winglang
- You'll interact with it in the embedded Wing Simulator, and then compile to AWS. + You'll create resources, explore the terraform output, interact with the Simulator and have fune
) -} \ No newline at end of file +} diff --git a/apps/tour/src/tutorials/01-tutorial.md b/apps/tour/src/tutorials/01-tutorial.md index bd0cf72b..3cf3c3cb 100644 --- a/apps/tour/src/tutorials/01-tutorial.md +++ b/apps/tour/src/tutorials/01-tutorial.md @@ -1,7 +1,9 @@ -# Let's add the first cloud service to our app +# Cloud.Queue resource Below is an empty Wing source code file. -The first line, "bring cloud", lets us access resources from the Wing standard library. -1. **Uncomment line 3**. A **queue** will appear in the simulator to the right. -2. Click **Next** to continue +1. Uncomment line 3 +2. The code compiles into terraform, see generate SQS queue on the right +3. Click **Next** to continue + + diff --git a/apps/tour/src/tutorials/02-solution.w b/apps/tour/src/tutorials/02-solution.w new file mode 100644 index 00000000..74f72a29 --- /dev/null +++ b/apps/tour/src/tutorials/02-solution.w @@ -0,0 +1,8 @@ +bring cloud; + +let q = new cloud.Queue(); + +let handler = inflight (s: str) => { + log("inflight function was called with ${s}"); + q.push(s); +}; diff --git a/apps/tour/src/tutorials/02-tutorial.md b/apps/tour/src/tutorials/02-tutorial.md index 438b7b6d..e58d275d 100644 --- a/apps/tour/src/tutorials/02-tutorial.md +++ b/apps/tour/src/tutorials/02-tutorial.md @@ -1,6 +1,14 @@ -# Push a message to the queue +# Inflight function compiles to javascript code -1. In the Wing Simulator, **click on the resource in the center named "cloud.Queue"**. An interaction panel will appear on the right hand side. -2. Under **Push Message**, type in a message and click **Send**. +inflight functions are functions that are going to run on the cloud + +1. Paste this inflight function in line 4: +```ts +let handler = inflight (s: str) => { + log("inflight function was called with ${s}"); + // Type 'q.' to see the available methods +}; +``` +2. The inflight code compiled into JS, Look for the code under assets +3. Add code to push string s into `q` in line 7 -πŸ” Notice how the **Approx size** of the queue has changed. diff --git a/apps/tour/src/tutorials/03-code.w b/apps/tour/src/tutorials/03-code.w index 3b6b3503..74f72a29 100644 --- a/apps/tour/src/tutorials/03-code.w +++ b/apps/tour/src/tutorials/03-code.w @@ -1,3 +1,8 @@ bring cloud; -let q = new cloud.Queue(); \ No newline at end of file +let q = new cloud.Queue(); + +let handler = inflight (s: str) => { + log("inflight function was called with ${s}"); + q.push(s); +}; diff --git a/apps/tour/src/tutorials/03-solution.w b/apps/tour/src/tutorials/03-solution.w index 880477cf..68ed22f9 100644 --- a/apps/tour/src/tutorials/03-solution.w +++ b/apps/tour/src/tutorials/03-solution.w @@ -2,6 +2,9 @@ bring cloud; let q = new cloud.Queue(); -new cloud.Function(inflight (s: str) => { - log("Cloud Function was called with ${s}"); -}); \ No newline at end of file +let handler = inflight (s: str) => { + log("inflight function was called with ${s}"); + q.push(s); +}; + +new cloud.Function(handler); diff --git a/apps/tour/src/tutorials/03-tutorial.md b/apps/tour/src/tutorials/03-tutorial.md index ff5428ea..f738a30f 100644 --- a/apps/tour/src/tutorials/03-tutorial.md +++ b/apps/tour/src/tutorials/03-tutorial.md @@ -1,11 +1,12 @@ -# Invoke a function and explore the Logs Panel +# Use cloud.Function for invoking the handler -1. Paste this code in line 4: +Let's wrap the inflight code with a cloud.Function + +1. Add a cloud.Function resource that will use the handler ```ts -new cloud.Function(inflight (s: str) => { - log("Cloud Function was called with ${s}"); -}); +new cloud.Function(handler); ``` -2. Find the **cloud.Function** in the simulator and **invoke** it with a payload. +2. Notice that there are a lot of new resources, including IAM permissions +3. In IAM policy, find the `sqs:SendMessage` permission generated for the cloud.Function -πŸ” Notice the resulting log at the bottom of the simulator. +Next, lets build this code for a different target, Wing Simulator diff --git a/apps/tour/src/tutorials/04-code.w b/apps/tour/src/tutorials/04-code.w index d91c1411..68ed22f9 100644 --- a/apps/tour/src/tutorials/04-code.w +++ b/apps/tour/src/tutorials/04-code.w @@ -2,16 +2,9 @@ bring cloud; let q = new cloud.Queue(); -new cloud.Function(inflight (s: str) => { - log("Cloud Function was called with ${s}"); - // Type 'q.' to see the available methods -}); +let handler = inflight (s: str) => { + log("inflight function was called with ${s}"); + q.push(s); +}; -// Type 'q.' to see the available methods here. -// Notice how they are different from the ones -// suggested within the Cloud Function in line 7. -// It's because even though it's the same queue, -// lines 7 & 10 are in different execution phases. -// preflight - for infrastructure definitions, executed at compile time. -// inflight - for business logic, executed at runtime. -// We'll learn more about these concepts in the next tutorial. \ No newline at end of file +new cloud.Function(handler); diff --git a/apps/tour/src/tutorials/04-solution.w b/apps/tour/src/tutorials/04-solution.w deleted file mode 100644 index 2b2cd586..00000000 --- a/apps/tour/src/tutorials/04-solution.w +++ /dev/null @@ -1,17 +0,0 @@ -bring cloud; - -let q = new cloud.Queue(); - -new cloud.Function(inflight (s: str) => { - log("Cloud Function was called with ${s}"); - q.push(s); -}); - -// Type 'q.' to see the available methods here. -// Notice how they are different from the ones -// suggested within the Cloud Function in line 7. -// It's because even though it's the same queue, -// lines 7 & 10 are in different execution phases. -// preflight - for infrastructure definitions, executed at compile time. -// inflight - for business logic, executed at runtime. -// We'll learn more about these concepts in the next tutorial. \ No newline at end of file diff --git a/apps/tour/src/tutorials/04-tutorial.md b/apps/tour/src/tutorials/04-tutorial.md index ed91e1ea..1264672e 100644 --- a/apps/tour/src/tutorials/04-tutorial.md +++ b/apps/tour/src/tutorials/04-tutorial.md @@ -1,7 +1,9 @@ -# Push a message to the queue programmatically +# Wing Simulator -1. Add code to push the function's payload to the queue in line 7. (Hint: look at how the payload is added to the log in line 6). -2. Invoke the cloud.Function in the simulator (notice it's connected to the queue now). -3. Explore what happens in the cloud.Queue in the simulator. + +1. In the Wing Simulator, click on the resource in the center named "cloud.Function". +2. An interaction panel will appear on the right hand side. +3. Under Invoke, type in a message and click Send. +4. In the Wing Simulator, click on the resource in the center named "cloud.Queue" +5. Notice how the **Approx size** increases every time you call it -πŸ† **Bonus:** Look at the code comment in line 10. \ No newline at end of file diff --git a/apps/tour/src/tutorials/05-code.w b/apps/tour/src/tutorials/05-code.w index 834be567..deb69a97 100644 --- a/apps/tour/src/tutorials/05-code.w +++ b/apps/tour/src/tutorials/05-code.w @@ -4,6 +4,6 @@ let q = new cloud.Queue(); // Paste here new cloud.Function(inflight (s: str) => { - log("Cloud Function was called with ${s}"); - q.push(s); -}); \ No newline at end of file + log("inflight function was called with ${s}"); + q.push(s); +}); diff --git a/apps/tour/src/tutorials/05-solution.w b/apps/tour/src/tutorials/05-solution.w index 5e839b82..b72627d5 100644 --- a/apps/tour/src/tutorials/05-solution.w +++ b/apps/tour/src/tutorials/05-solution.w @@ -4,10 +4,10 @@ let q = new cloud.Queue(); let b = new cloud.Bucket() as "Bucket: Last Message"; q.addConsumer(inflight (m: str) => { - b.put("latest.txt", m); + b.put("latest.txt", m); }); new cloud.Function(inflight (s: str) => { - log("Cloud Function was called with ${s}"); - q.push(s); -}); \ No newline at end of file + log("inflight function was called with ${s}"); + q.push(s); +}); diff --git a/apps/tour/src/tutorials/05-tutorial.md b/apps/tour/src/tutorials/05-tutorial.md index 4a826332..97f57136 100644 --- a/apps/tour/src/tutorials/05-tutorial.md +++ b/apps/tour/src/tutorials/05-tutorial.md @@ -1,5 +1,7 @@ # Add a Bucket to store the latest message sent to our queue. +To make things shorter we've inlined the inflight handler + 1. Paste this code in line 4. ```ts let b = new cloud.Bucket() as "Bucket: Last Message"; diff --git a/apps/tour/src/tutorials/06-code.w b/apps/tour/src/tutorials/06-code.w index 5e839b82..3b83433c 100644 --- a/apps/tour/src/tutorials/06-code.w +++ b/apps/tour/src/tutorials/06-code.w @@ -4,10 +4,10 @@ let q = new cloud.Queue(); let b = new cloud.Bucket() as "Bucket: Last Message"; q.addConsumer(inflight (m: str) => { - b.put("latest.txt", m); + b.put("latest.txt", m); }); new cloud.Function(inflight (s: str) => { - log("Cloud Function was called with ${s}"); - q.push(s); -}); \ No newline at end of file + log("Cloud Function was called with ${s}"); + q.push(s); +}); diff --git a/apps/tour/src/tutorials/index.ts b/apps/tour/src/tutorials/index.ts index 8b179e1c..3adf6d14 100644 --- a/apps/tour/src/tutorials/index.ts +++ b/apps/tour/src/tutorials/index.ts @@ -1,3 +1,4 @@ +import { Target } from "@wing-playground/shared/src/compiler/compiler"; import code01 from "./01-code.w?raw"; import solution01 from "./01-solution.w?raw"; import tutorial01 from "./01-tutorial.md?raw"; @@ -28,6 +29,7 @@ export interface Tutorial { code: string; solution?: string; tutorial?: string + targets?: Target[]; } export const tutorials = [ @@ -36,41 +38,46 @@ export const tutorials = [ name: "Create Queue", code: code01, solution: solution01, - tutorial: tutorial01 + tutorial: tutorial01, + targets: [Target.TFAWS], }, { id: "2", - name: "Push Message", + name: "Inflight Code", code: code02, - // solution: solution02, - tutorial: tutorial02 + solution: solution02, + tutorial: tutorial02, + targets: [Target.TFAWS], }, { id: "3", - name: "Logs", + name: "cloud.Function", code: code03, solution: solution03, - tutorial: tutorial03 + tutorial: tutorial03, + targets: [Target.TFAWS], }, { id: "4", - name: "Push Programmatically", + name: "Simulator", code: code04, - solution: solution04, - tutorial: tutorial04 + tutorial: tutorial04, + targets: ["simulator", Target.TFAWS], }, { id: "5", name: "Use Bucket", code: code05, solution: solution05, - tutorial: tutorial05 + tutorial: tutorial05, + targets: ["simulator", Target.TFAWS], }, { id: "6", name: "Compile for AWS", code: code06, // solution: solution06, - tutorial: tutorial06 + tutorial: tutorial06, + targets: ["simulator", Target.TFAWS], }, -] \ No newline at end of file +] diff --git a/packages/shared/src/Tabs.tsx b/packages/shared/src/Tabs.tsx new file mode 100644 index 00000000..f31634ed --- /dev/null +++ b/packages/shared/src/Tabs.tsx @@ -0,0 +1,116 @@ +import classNames from "classnames"; +import { ReactNode, useEffect, useState } from "react"; + +export interface Tab { + id: string; + name: string; + icon?: ReactNode; + panel?: ReactNode | (() => ReactNode); + count?: number; + tabClassName?: string; +} + +export interface TabsProps { + tabs: Tab[]; + currentTabId?: string; + onTabChange?: (tabId: string) => void; + renderActiveTabPanelOnly?: boolean; + tabsWithNotifications?: string[]; + className?: string; +} + +export const Tabs = (props: TabsProps) => { + const [currentTabId, setCurrentTabId] = useState(props.currentTabId); + + useEffect(() => { + if (props.currentTabId) { + setCurrentTabId(props.currentTabId); + } + }, [props]); + + useEffect(() => { + if (props.onTabChange && currentTabId) { + props.onTabChange(currentTabId); + } + }, [currentTabId]); + + return ( +
+
+
+ {props.tabs.map((tab) => { + const isCurrent = tab.id === currentTabId; + return ( +
setCurrentTabId(tab.id)} + > + {tab.icon &&
{tab.icon}
} +
+ {tab.name} + {tab.count !== undefined && ( + ({tab.count}) + )} +
+ + {props.tabsWithNotifications?.includes(tab.id) && ( +
+ + + + +
+ )} +
+ ); + })} +
+
+ + {props.tabs.map((tab) => { + const isCurrent = tab.id === currentTabId; + if (props.renderActiveTabPanelOnly && !isCurrent) { + return; + } + + return ( +
+ {typeof tab.panel === "function" ? tab.panel() : tab.panel} +
+ ); + })} +
+ ); +}; diff --git a/packages/shared/src/compiler/compiler.ts b/packages/shared/src/compiler/compiler.ts index 8830c9b4..77d52efd 100644 --- a/packages/shared/src/compiler/compiler.ts +++ b/packages/shared/src/compiler/compiler.ts @@ -15,7 +15,7 @@ export interface CompilationItem { export interface CompilationResult { files: CompilationItem[]; - zip: Zip; + zip: Zip; error?: { stderr: string; stdout: string; @@ -115,4 +115,4 @@ export class Compiler { this.compilations.set(sha, compile(request.code, request.target)); } -} \ No newline at end of file +} diff --git a/packages/shared/src/editor/use-editor.tsx b/packages/shared/src/editor/use-editor.tsx index 4bee20f8..c12c0510 100644 --- a/packages/shared/src/editor/use-editor.tsx +++ b/packages/shared/src/editor/use-editor.tsx @@ -10,27 +10,43 @@ import {LoadingStatus} from "../loading-status"; import {LanguageContext} from "../use-examples"; import {debounce} from "lodash"; import {CompilationRequest} from "../compiler/request"; -import {Compiler, Target} from "../compiler/compiler"; -import React, {useRef, useState} from "react"; +import {CompilationItem, Compiler, Target} from "../compiler/compiler"; +import {useRef, useState, MutableRefObject, useEffect} from "react"; import * as monaco from 'monaco-editor'; - export interface UseEditorOptions { - editorRef: React.MutableRefObject; + editorRef: MutableRefObject; onLoadingStatusChange: (state: LoadingStatus) => void; onLspError: () => void; code: string; languageContext: LanguageContext; compiler: Compiler; + targets?: Target[]; editorOptions: monaco.editor.IStandaloneEditorConstructionOptions; - installConsole?: (containerRef: React.MutableRefObject) => Promise; + installConsole?: (containerRef: MutableRefObject) => Promise; editorTheme?: string; shouldInitContainer: boolean; } +export type CompilerOutput = { + target: Target, + files: CompilationItem[], +} + const darkPlusTheme = convertTheme(darkPlusTMTheme); -export const useEditor = ({editorRef, onLoadingStatusChange, onLspError, code, languageContext, compiler, installConsole, editorTheme, shouldInitContainer}: UseEditorOptions) => { +export const useEditor = ({ + editorRef, + onLoadingStatusChange, + onLspError, + code, + languageContext, + compiler, + targets = [], + installConsole, + editorTheme, + shouldInitContainer +}: UseEditorOptions) => { const [isCompiling, setIsCompiling] = useState(false); const containerRef = useRef(); @@ -89,14 +105,27 @@ export const useEditor = ({editorRef, onLoadingStatusChange, onLspError, code, l return; } console.log('evaluating...', languageContext) - setIsCompiling(true) + setIsCompiling(true); + try { - let compileValue = editorRef.current?.getValue() - await prepareForEvaluation(containerRef.current, compileValue, languageContext.file) - await compiler.submit(new CompilationRequest(compileValue!, Target.TFAWS)); - } finally { + let compileValue = editorRef.current?.getValue() + await prepareForEvaluation(containerRef.current, compileValue, languageContext.file) + + targets.forEach(async (target, index) => { + compiler.submit(new CompilationRequest(compileValue!, target)); + if (index === targets.length - 1) { + onLoadingStatusChange(LoadingStatus.Completed) + setIsCompiling(false) + } + }); + if (targets.length === 0) { onLoadingStatusChange(LoadingStatus.Completed) setIsCompiling(false) + } + } catch (error) { + console.error(error); + onLoadingStatusChange(LoadingStatus.CompileError) + setIsCompiling(false) } }, 700);