Skip to content

Commit

Permalink
Merge pull request #1 from emergentmethods/feat/triggers
Browse files Browse the repository at this point in the history
Feat/triggers
  • Loading branch information
wagnercosta authored Jul 12, 2024
2 parents 3051475 + 687e4ac commit a8e281f
Show file tree
Hide file tree
Showing 20 changed files with 765 additions and 124 deletions.
70 changes: 1 addition & 69 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,22 +109,6 @@ Use PascalCase for component names and class names.
Make variable and function names descriptive and concise.
Use UPPER_SNAKE_CASE for constants.

### 3. ESLint Configuration

- ...

### 4. Prettier Integration

- ...

### 5. Pre-commit Hooks

- ...

### 6. TypeScript (optional)

- ...

### ESLint Rules

| Rule | Value | Explanation |
Expand Down Expand Up @@ -194,7 +178,7 @@ We highly value clear and concise documentation in our codebase. We use TSDoc to

Here's an example of a well-documented function using TSDoc:

````typescript
```typescript
/**
* Calculate the sum of two numbers.
*
Expand All @@ -210,60 +194,8 @@ Here's an example of a well-documented function using TSDoc:
export function add(a: number, b: number): number {
return a + b;
}
````

## Tests

### Unit Tests

React testing library and Jest

### e2e Tests

#### Playwright

To install Playwright, use the command `npx playwright install`.

We're employing a HTTP server with mock data to handle API call mocking. This is implemented via the code in `./mockServer`. The mock server's responses can be customized in `mockServer/main.ts`.

Here are the commands for running the e2e tests:

- `npm run test:e2e`: Initiates the mock server and executes the tests in development mode, with hot reloading enabled.
- `npm run test:e2e:ui`: Functions like the previous command, but also opens the browser.
- `npm run test:e2e:build`: Activates the mock server and carries out the tests in build mode without hot reloading. Although the initialization for this command might take longer, it's useful for verifying whether the tests operate correctly in production mode. After the build process, test execution speeds up.
- `e2e`: Executes the tests using the Flowdapt backend without the mock server.
- `e2e:ui`: Similar to the previous command but it also opens the browser.

#### MSW

MSW (Mock Service Worker) is installed with the primary intention to intercept server-side calls and simplify mock creation. However, at present, MSW doesn't support the Next.JS app directory, hence we're not utilizing it.

Follow the issue here:

[Support Next.js 13 (App directory)](https://github.com/mswjs/msw/issues/1644)

To record server API calls, add this code in `src/app/layout.tsx` (after the imports - top of the file):

```typescript
if (process.env.NEXT_PUBLIC_API_MOCKING_RECORD === "enabled") {
require("../../mocks");
}
```

And add this line in `.env.local` file:

```env
NEXT_PUBLIC_API_MOCKING_RECORD="enabled"
```

## License

Include your project's license information.

## Flowdapt references

- For icons: [Hero Icons](https://heroicons.com)

## Theme Modification Guide

This project utilizes [Tailwind CSS](https://tailwindcss.com/) and [DaisyUI](https://daisyui.com) for theming.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"@codemirror/lang-json": "^6.0.1",
"@codemirror/language": "^6.8.0",
"@codemirror/legacy-modes": "^6.3.2",
"@emergentmethods/flowdapt-ts-sdk": "^1.0.5",
"@emergentmethods/flowdapt-ts-sdk": "^1.0.6",
"@heroicons/react": "^2.0.18",
"@hookform/resolvers": "^3.1.0",
"@tremor/react": "^3.4.1",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion src/app/[lang]/components/LeftMenu/MenuData.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ForwardIcon, HomeIcon, CogIcon } from "@heroicons/react/24/outline";
import { ForwardIcon, HomeIcon, CogIcon, FireIcon } from "@heroicons/react/24/outline";
import { ReactNode } from "react";

export const menuData: {
Expand All @@ -21,4 +21,9 @@ export const menuData: {
link: "/config",
Icon: <CogIcon className="w-7 h-7" />,
},
{
label: "Triggers",
link: "/trigger",
Icon: <FireIcon className="w-7 h-7" />,
},
];
73 changes: 73 additions & 0 deletions src/app/[lang]/trigger/components/TriggerList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"use server";

import { Locale, getDictionary } from "@/i18n/dictionaries";
import TriggerRowActions from "./TriggerRowActions";
import { ITable, ITableOptions } from "@/components/tables/utils";
import Table from "@/components/tables";
import TriggerPageActions from "./TriggerPageActions";

export type TriggerData = {
name: string;
type: "schedule" | "condition";
uid?: string | undefined;
created_at?: string | undefined;
updated_at?: string | undefined;
annotations?: Record<string, string> | undefined;
group?: string;
};

interface IConfigListProps {
triggerPromise: Promise<TriggerData[]>;
lang: Locale;
tableOptions: ITableOptions<TriggerData>;
}

const TriggerList = async (props: IConfigListProps) => {
const { lang, tableOptions, triggerPromise } = props;
const dict = getDictionary(lang);
const triggerData = await triggerPromise;
const localDict = dict["trigger"];
const globalDict = dict["global"];

const tableProps: ITable<TriggerData> = {
tableOptions,
data: triggerData,
tableHeaders: [
{
fieldName: "name",
label: localDict.name,
},
{
fieldName: "type",
label: localDict.type,
},
{
fieldName: "uid",
label: globalDict.uid,
},
{
fieldName: "group",
label: globalDict.group,
},
{
fieldName: "created_at",
label: globalDict.createdAt,
fieldType: "date",
},
],
RowActions: TriggerRowActions,
actionsLabel: globalDict.actions,
pathname: "config",
selectRowOptions: {
enabled: true,
fieldKey: "name",
},
pageHeaderOptions: {
title: localDict.title,
rightElement: <TriggerPageActions />,
},
};
return <Table {...tableProps} />;
};

export default TriggerList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"use client";

import Modal from "@/components/common/Modal";
import useDictionaries from "@/hooks/useDictionaries";
import { useRouter } from "next/navigation";
import { useState, useTransition } from "react";
import { deleteAllTriggers } from "../../server-actions";

interface IModalDeleteSelectedConfigs {
configNames: string[];
modalDeleteAllId: string;
}
const ModalDeleteSelectedTriggers = (props: IModalDeleteSelectedConfigs) => {
const { modalDeleteAllId, configNames } = props;
const router = useRouter();
const dict = useDictionaries();
const [isPending, startTransition] = useTransition();
const [isFetching, setIsFetching] = useState(false);
const isMutating = isFetching || isPending;
const deleteAll = async () => {
setIsFetching(true);

await deleteAllTriggers(configNames);
setIsFetching(false);
startTransition(() => {
// Refresh the current route:
// - Makes a new request to the server for the route
// - Re-fetches data requests and re-renders Server Components
// - Sends the updated React Server Component payload to the client
// - The client merges the payload without losing unaffected
// client-side React state or browser state
router.refresh();

// Note: If fetch requests are cached, the updated data will
// produce the same result.
});
};

return (
<Modal
actionCallback={deleteAll}
action={dict.global.delete}
cancel={dict.global.cancel}
description={dict.trigger.modalConfirmDeleteSelected}
modalId={modalDeleteAllId}
title={dict.trigger.modalDeleteSelectedTitle}
isMutating={isMutating}
/>
);
};

export default ModalDeleteSelectedTriggers;
71 changes: 71 additions & 0 deletions src/app/[lang]/trigger/components/TriggerPageActions/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use client";

import DropdownPageActions from "../../../../../components/DropdownPageActions";
import ModalDeleteSelectedTriggers from "./ModalDeleteSelectedTriggers";
import useDictionaries from "@/hooks/useDictionaries";
import Link from "next/link";
import { ReactNode, useContext } from "react";
import { LanguageDictType } from "@/i18n/dictionaries";
import { useRouter } from "next/navigation";
import InputTableSearch from "@/components/tables/InputTableSearch";
import { TableContext } from "@/components/tables/TableContext";
import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";

export const getDropdownItems = (
itemsSelected: string[],
dict: LanguageDictType,
router: AppRouterInstance,
modalDeleteAllId: string
) => {
const dropdownItems: ReactNode[] = [];
if (itemsSelected.length > 0) {
dropdownItems.push(
<button
type="button"
key={"deleteAll"}
onClick={() => window[modalDeleteAllId].showModal()}
className="justify-between"
>
{dict.global.delete}
</button>
);
}
dropdownItems.push(
<Link href={"/trigger/yaml/new"} key={"newyaml"} className="justify-between">
{dict.configs.newFromYaml}
</Link>
);

dropdownItems.push(
<button className="justify-between" key={"refresh"} onClick={() => router.refresh()}>
{dict.global.refresh}
</button>
);

return dropdownItems;
};

const ConfigPageActions = () => {
const dict = useDictionaries();
const modalDeleteAllId = "delete-all-modal";
const router = useRouter();

const tableContext = useContext(TableContext);
const { allSelectedItems = [] } = tableContext || {};

const dropdownItems = getDropdownItems(allSelectedItems, dict, router, modalDeleteAllId);

return (
<div>
<InputTableSearch />
<DropdownPageActions title={dict.global.actions} dropdownItems={dropdownItems}>
<ModalDeleteSelectedTriggers
modalDeleteAllId={modalDeleteAllId}
configNames={allSelectedItems}
/>
</DropdownPageActions>
</div>
);
};

export default ConfigPageActions;
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client";

import Modal from "@/components/common/Modal";
import useDictionaries from "@/hooks/useDictionaries";
import { useRouter } from "next/navigation";
import { deleteTrigger } from "../../server-actions";

interface IModalDeleteTrigger {
triggerName: string;
modalId: string;
}
const ModalDeleteTrigger = (props: IModalDeleteTrigger) => {
const { triggerName, modalId } = props;
const router = useRouter();
const dict = useDictionaries();

const deleteWithId = async () => {
await deleteTrigger(triggerName);
router.refresh();
};

return (
<Modal
actionCallback={deleteWithId}
action={dict.global.delete}
cancel={dict.global.cancel}
description={dict.trigger.modalConfirmDeleteRow}
modalId={modalId}
title={dict.trigger.modalDeleteRowTitle}
/>
);
};

export default ModalDeleteTrigger;
Loading

0 comments on commit a8e281f

Please sign in to comment.