Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sequencer Gallery #1169

Merged
merged 18 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,13 @@ export default {
openAllFilesInFolder: (
folderPath: string,
allowedExtensions: string[] = ["json"],
relativePath: boolean = false,
): Promise<{ filePath: string; fileContent: string }[] | undefined> =>
ipcRenderer.invoke(
API.openAllFilesInFolderPicker,
folderPath,
allowedExtensions,
relativePath,
),

getFileContent: (filepath: string): Promise<string> =>
Expand Down
4 changes: 4 additions & 0 deletions src/main/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,12 @@ export const openAllFilesInFolderPicker = (
_,
folderPath: string,
allowedExtensions: string[] = ["json"],
relativePath: boolean = false,
): { filePath: string; fileContent: string }[] | undefined => {
// Return multiple files or all files with the allowed extensions if a folder is selected
if (relativePath) {
folderPath = join(__dirname, folderPath);
}
if (!fs.existsSync(folderPath) || !fs.lstatSync(folderPath).isDirectory()) {
return undefined;
}
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/assets/FlojoyTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const flojoySyntaxTheme: SyntaxTheme = {
background: "rgb(var(--color-modal))",
},
"hljs-comment": {
color: "rgb(var(--foreground))",
color: "rgb(var(--color-accent4))",
fontStyle: "italic",
},
"hljs-quote": {
Expand Down
LatentDream marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"Conditional Demo","description":"Example","elems":[{"type":"test","id":"ca204090-41bb-4236-97ba-623c4257ef0a","groupId":"41717403-d6ba-49c3-b451-a4a0ae815b2c","path":"test.py::test_will_pass","testName":"test_will_pass","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"5ec59132-f8f5-45be-b0ad-31124a3a6ba0","role":"start","groupId":"0a78f451-a9b6-4fa9-9fe3-16dea93fa46b","conditionalType":"if","condition":" $test_will_pass"},{"type":"test","id":"67014fb0-9a93-433b-8398-df103de3dcc1","groupId":"8a8ecab5-b765-4944-a46c-63111fd59e03","path":"test.py::test_for_example_1","testName":"test_for_example_1","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"test","id":"7d6d00ee-f418-49d4-9bcd-5b9cc2e28470","groupId":"e2de5be6-ba8c-4aa8-b6ac-b32b1f30dec1","path":"test.py::test_will_fail","testName":"test_will_fail","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"5ec59132-f8f5-45be-b0ad-31124a3a6ba0","role":"start","groupId":"a45f0b58-b74e-4626-8a0a-2dec3d8e9cfa","conditionalType":"if","condition":" $test_will_fail"},{"type":"test","id":"b4927c15-00e8-4ea4-8788-af395be14775","groupId":"b824ead8-84ad-4de3-88c1-21e4b46343fe","path":"test.py::test_for_example_2","testName":"test_for_example_2","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"d5b1cf12-f50d-4f5a-88d4-b7a9f982a447","role":"between","groupId":"a45f0b58-b74e-4626-8a0a-2dec3d8e9cfa","conditionalType":"else","condition":""},{"type":"test","id":"5ad4da63-40fe-45a2-ab1e-235fd067bde2","groupId":"8f02f062-19f5-448f-83b4-1fcc4e9dc07a","path":"test.py::test_for_example_3","testName":"test_for_example_3","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"c45616fd-cc5a-4469-9bb6-5f3a8ae74bd7","role":"end","groupId":"a45f0b58-b74e-4626-8a0a-2dec3d8e9cfa","conditionalType":"end","condition":""},{"type":"conditional","id":"d5b1cf12-f50d-4f5a-88d4-b7a9f982a447","role":"between","groupId":"0a78f451-a9b6-4fa9-9fe3-16dea93fa46b","conditionalType":"else","condition":""},{"type":"test","id":"01cafc7f-0c21-47b9-8246-305b36a0c23a","groupId":"2e320c36-4b8b-48ec-97af-732e623c3776","path":"test.py::test_for_example_4","testName":"test_for_example_4","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T15:57:40.507Z"},{"type":"conditional","id":"c45616fd-cc5a-4469-9bb6-5f3a8ae74bd7","role":"end","groupId":"0a78f451-a9b6-4fa9-9fe3-16dea93fa46b","conditionalType":"end","condition":""}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/src/renderer/data/apps/sequencer/conditional/","interpreter":{"type":"flojoy","path":null,"requirementsPath":"flojoy_requirements.txt"}}
Empty file.
28 changes: 28 additions & 0 deletions src/renderer/data/apps/sequencer/conditional/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
Simple test file to demo how to build conditional tests
- With pytest, all test files should start with 'test_' to be recognized
"""


def test_will_pass():
assert True


def test_will_fail():
assert False


def test_for_example_1():
assert 1 == 1


def test_for_example_2():
assert 2 == 2


def test_for_example_3():
assert 3 == 3


def test_for_example_4():
assert 4 == 4
LatentDream marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"Export & Expected Demo","description":"Consult the Test Step Code!","elems":[{"type":"test","id":"acb47b74-d1dc-4f4b-b3ef-506acb2f53e1","groupId":"f90f758a-a705-4dd0-b3b6-4ae032d22ea3","path":"test.py::test_min_max","testName":"test_min_max","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z","minValue":5,"maxValue":10,"unit":""},{"type":"test","id":"53bb5746-c720-45c4-8121-9dc469d07927","groupId":"397475ec-c600-4478-8725-b7b8e824d599","path":"test.py::test_min","testName":"test_min","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z","minValue":5,"unit":""},{"type":"test","id":"55b82abb-37fb-4e32-9379-3609642d01af","groupId":"0377f366-edb7-4a51-97cd-2867bd944cd2","path":"test.py::test_max","testName":"test_max","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z","maxValue":7,"unit":""},{"type":"test","id":"029a20ad-4c3f-47a5-82aa-2d558eda418a","groupId":"97ba9e80-3133-4dfa-9806-770d47ffb1e4","path":"test.py::test_export_dataframe","testName":"test_export_dataframe","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z"},{"type":"test","id":"2a75451e-49f6-4810-827e-2b9769d12bc4","groupId":"096274ef-132f-498a-a67a-a279c65b9a8a","path":"test.py::test_export","testName":"test_export","runInParallel":false,"testType":"pytest","status":"pending","error":null,"isSavedToCloud":false,"exportToCloud":true,"createdAt":"2024-04-20T16:13:35.911Z"}],"projectPath":"C:/Users/zzzgu/Documents/flojoy/repo/studio/src/renderer/data/apps/sequencer/expected_exported_values/","interpreter":{"type":"flojoy","path":null,"requirementsPath":"flojoy_requirements.txt"}}
48 changes: 48 additions & 0 deletions src/renderer/data/apps/sequencer/expected_exported_values/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from flojoy_cloud import test_sequencer
import pandas as pd


def test_min_max():
value = 6.15
test_sequencer.export(value)
assert test_sequencer.is_in_range(value)


def test_min():
value = 6.15
# If not Max value is defined, the value will be checked against the Min value.
test_sequencer.export(value)
assert test_sequencer.is_in_range(value)


def test_max():
value = 6.15
test_sequencer.export(value)

assert test_sequencer.is_in_range(value)
# If multiple assert statements are defined and one of them fails:
# - the rest of the assert statements will not be executed, and the result will
# be reported to the sequencer.
# - the sequencer will report the error, and the test will be marked as failed.
assert 0 < value


def test_export_dataframe():
df = pd.DataFrame({"value": [6.15, 6.15, 6.15]})
# Boolean and DataFrame values will be exported to the Cloud.
test_sequencer.export(df)

assert df is not None


def test_export():
value = 6.15
# Always export as early as possible to avoid missing data.
test_sequencer.export(value)
assert 12 < value # <-- FAIL

# Only the last executed export statement will be exported to the Cloud and
# reported to the sequencer.
test_sequencer.export(20)

assert 0 < value
55 changes: 55 additions & 0 deletions src/renderer/hooks/useTestSequencerProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,61 @@ export const useImportSequences = () => {
return handleImport;
};

export const useImportAllSequencesInFolder = () => {
const manager = usePrepareStateManager();
const { isAdmin } = useWithPermission();

const handleImport = async (path: string, relative: boolean = false) => {
async function importSequences(): Promise<Result<void, Error>> {
// Confirmation if admin
if (!isAdmin()) {
return err(
Error(
"Admin only, Connect to Flojoy Cloud and select a Test Profile",
),
);
}

// Find .tjoy files from the profile
const result = await window.api.openAllFilesInFolder(
path,
["tjoy"],
relative,
);
if (result === undefined) {
return err(Error(`Failed to find the directory ${path}`));
}
if (!result || result.length === 0) {
return err(Error("No .tjoy file found in the selected directory"));
}

// Import them in the sequencer
await Promise.all(
result.map(async (res, idx) => {
const { filePath, fileContent } = res;
const result = await importSequence(
filePath,
fileContent,
manager,
idx !== 0,
);
if (result.isErr()) return err(result.error);
}),
);

return ok(undefined);
}

toastResultPromise(importSequences(), {
loading: `Importing Sequences...`,
success: () => `Sequences imported`,
error: (e) => `${e}`,
});
};

return handleImport;
};

export const useLoadTestProfile = () => {
const manager = usePrepareStateManager();
const { isAdmin } = useWithPermission();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useSequencerModalStore } from "@/renderer/stores/modal";
import { useDisplayedSequenceState } from "@/renderer/hooks/useTestSequencerState";
import { Button } from "@/renderer/components/ui/button";
import { ACTIONS_HEIGHT } from "@/renderer/routes/common/Layout";
import { FlaskConical, Import, LayoutGrid, Plus, Route } from "lucide-react";
import { FlaskConical, Import, Plus, Route } from "lucide-react";
import {
StatusType,
Test,
Expand All @@ -28,6 +28,7 @@ import {
HoverCardTrigger,
} from "@/renderer/components/ui/hover-card";
import _ from "lodash";
import { SequencerGalleryModal } from "./modals/SequencerGalleryModal";

export function DesignBar() {
const { setIsImportTestModalOpen, setIsCreateProjectModalOpen } =
Expand Down Expand Up @@ -63,6 +64,7 @@ export function DesignBar() {
}, [elems, sequences, cycleRuns]);

const [displayTotal, setDisplayTotal] = useState(false);
const [isGalleryOpen, setIsGalleryOpen] = useState(false);

return (
<div className=" border-b" style={{ height: ACTIONS_HEIGHT }}>
Expand Down Expand Up @@ -110,15 +112,13 @@ export function DesignBar() {
<Import size={16} className="mr-2 stroke-muted-foreground" />
Import Sequence
</DropdownMenuItem>
<DropdownMenuItem disabled={true}>
<LayoutGrid
size={16}
className="mr-2 stroke-muted-foreground"
/>
Sequence Gallery
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<SequencerGalleryModal
isGalleryOpen={isGalleryOpen}
setIsGalleryOpen={setIsGalleryOpen}
/>

<div className="grow" />
</>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/renderer/components/ui/dialog";
import { LayoutGrid } from "lucide-react";
import { ScrollArea } from "@/renderer/components/ui/scroll-area";
import { Separator } from "@/renderer/components/ui/separator";
import { Button } from "@/renderer/components/ui/button";
import { useImportAllSequencesInFolder } from "@/renderer/hooks/useTestSequencerProject";

type AppGalleryModalProps = {
LatentDream marked this conversation as resolved.
Show resolved Hide resolved
isGalleryOpen: boolean;
setIsGalleryOpen: (open: boolean) => void;
};

export const SequencerGalleryModal = ({
isGalleryOpen,
setIsGalleryOpen,
}: AppGalleryModalProps) => {
const setOpen = () => {
setIsGalleryOpen(true);
};

const useImport = useImportAllSequencesInFolder();

const handleSequenceLoad = (BaseFolderName: string) => {
const relativePath = `src/renderer/data/apps/sequencer/${BaseFolderName}/`;
LatentDream marked this conversation as resolved.
Show resolved Hide resolved
useImport(relativePath);

Check failure on line 31 in src/renderer/routes/test_sequencer_panel/components/modals/SequencerGalleryModal.tsx

View workflow job for this annotation

GitHub Actions / ts-code-style

React Hook "useImport" is called in function "handleSequenceLoad" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"
setIsGalleryOpen(false);
};

const data = [
{
title: "Creating Sequences with Conditional",
description:
"Learn how to create a simple sequence with conditional logic.",
path: "conditional",
},
{
title: "Test Step with Expected and Exported Values",
description:
"Learn how to inject the minimum and maximum expected values into a test and export the result. Right-click on a test to consult the code and edit the expected values!",
path: "expected_exported_values",
},
];

return (
<Dialog open={isGalleryOpen} onOpenChange={setIsGalleryOpen}>
<DialogTrigger asChild>
<Button
onClick={setOpen}
data-testid="app-gallery-btn"
className="gap-2"
variant="ghost"
>
<LayoutGrid size={20} className="stroke-muted-foreground" />
Sequence Gallery
</Button>
</DialogTrigger>

<DialogContent className="flex h-4/5 max-w-5xl flex-col">
<DialogHeader>
<DialogTitle>
<div className="text-3xl">Sequence Gallery</div>
</DialogTitle>
</DialogHeader>
<ScrollArea className="">
{data.map((SeqExample) => (
LatentDream marked this conversation as resolved.
Show resolved Hide resolved
<>
<Separator />
<div className="1 inline-flex min-h-20 w-full items-center">
<div className="flex w-3/4">
<div className="flex grow flex-col items-start">
<div className="text-xl font-semibold">
{SeqExample.title}
</div>
<div className="text-sm font-thin">
{SeqExample.description}
</div>
</div>
</div>
<div className="grow" />
<Button
variant="outline"
size="sm"
className="ml-6 gap-2"
data-testid={SeqExample.title
.toLowerCase()
.split(" ")
.join("_")}
onClick={async () => {
await handleSequenceLoad(SeqExample.path);
}}
>
Load
</Button>
</div>
</>
))}
</ScrollArea>
</DialogContent>
</Dialog>
);
};
Loading