├── prettier.config.js
├── .babelrc
├── .env-example
├── .eslintrc
├── .gitignore
├── LICENSE
├── README.md
├── components
├── CollectionList.tsx
├── Editor.tsx
├── EventList.tsx
├── EventViewer.tsx
├── Modals
│ ├── CreateCollectionModal.tsx
│ ├── ImportEventModal.tsx
│ ├── NewEventModal.tsx
│ ├── PublishEventIntervalModal.tsx
│ ├── SaveEventModal.tsx
│ └── ViewEventLog.tsx
├── SideBar.tsx
└── TopNavigationBar.tsx
├── contexts
└── EventBridgeCanon.ts
├── data
└── .gitkeep
├── hooks
├── useApplication.ts
├── useCollections.ts
├── useEvents.ts
└── useLogs.ts
├── images
├── architecture.png
├── custom-event.png
├── editor-and-collections.png
├── editor.png
├── full-app-screenshot.png
└── logs.png
├── jsconfig.json
├── lib
├── aws.js
└── db.js
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
├── _app.tsx
├── api
│ ├── collections.ts
│ ├── collections
│ │ └── [collectionId]
│ │ │ └── events.ts
│ ├── data.ts
│ └── events
│ │ └── publish.ts
├── collection
│ └── [collectionId]
│ │ └── event
│ │ └── [eventId].tsx
├── events
│ └── create.tsx
└── index.js
├── postcss.config.js
├── public
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── canon.svg
├── eventbridge-icon.svg
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── logo-full.png
└── vercel.svg
├── scripts
└── populate.js
├── styles
└── globals.css
├── tailwind.config.js
├── tsconfig.json
├── types
└── index.ts
└── utils
├── clear-values.ts
└── request.ts
/ prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | semi: false,
3 | singleQuote: true,
4 | printWidth: 100,
5 | tabWidth: 2,
6 | useTabs: false,
7 | trailingComma: 'es5',
8 | bracketSpacing: true,
9 | };
10 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["next/babel"],
3 | "plugins": []
4 | }
5 |
--------------------------------------------------------------------------------
/.env-example:
--------------------------------------------------------------------------------
1 | EVENT_BUS_NAME=
2 | SCHEMA_REGISTRY_NAME=
3 | REGION=
4 |
5 | AWS_ACCESS_KEY_ID=
6 | AWS_SECRET_ACCESS_KEY=
7 | AWS_SESSION_TOKEN=
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next", "next/core-web-vitals"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 | .env
33 |
34 | package-lock.json
35 | yarn.lock
36 |
37 | # local lowdb database
38 | database.json
39 |
40 | # vercel
41 | .vercel
42 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) David Boyne
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Amazon EventBridge Canon 🗺
4 |
Simple UI to Publish, Save and Share Amazon EventBridge Events>
5 |
6 | [![MIT License][license-badge]][license]
7 | [![PRs Welcome][prs-badge]][prs]
8 |
9 | [![Watch on GitHub][github-watch-badge]][github-watch]
10 | [![Star on GitHub][github-star-badge]][github-star]
11 |
12 |
13 |
14 |
15 |
16 |
Features: Publish your events in seconds, Custom Editor to create and publish events, import/export events for your team, Prepopulated events, Fake data, Save events for future in collections, navigate and explore sources and events, and more...
17 |
18 | [Read the Docs](https://eventbridge-canon.netlify.app/) | [Edit the Docs](https://github.com/boyney123/eventbridge-canon-docs)
19 |
20 |
21 |
22 |
23 |
24 | # The problem
25 |
26 | I'm a huge fan of [Amazon EventBridge](https://aws.amazon.com/eventbridge/) and the ability it can give us to quickly create Event Driven Architectures and help businesses scale and grow.
27 |
28 | Like most things, over time you get more and more events you need to manage.
29 |
30 | So it's important to understand your event schema and also have the relevant tooling to help you stay productive.
31 |
32 | > **EventBridge Canon** was developed to help myself and hopefully others manage and publish events into EventBridge.
33 |
34 | I found myself wanting to easily create Events and publish them into EventBridge and reuse previous events I have sent. I stored events locally on my machine, but wanted a tool to help my own productivity.
35 |
36 | I wanted to create a solution that gives developers around the world the tools to **save events**, **re-publish** events and **manage events** whilst in development.
37 |
38 | # This solution
39 |
40 |
41 |
42 | **EventBridge Canon** is a GUI that was built to help developers publish EventBridge events.
43 |
44 | You could split **EventBridge Canon** into these parts.
45 |
46 | 1. AWS Integration (Fetching data from AWS)
47 | 2. LowDB (Storing data locally)
48 | 3. NextJS Application (Canon itself)
49 |
50 | You can read more on [how it works on the website](https://eventbridge-canon.netlify.app/docs/how-it-works)
51 |
52 | # Getting Started
53 |
54 | You should be able to get setup within minutes if you head over to our documentation to get started 👇
55 |
56 | ➡️ [Get Started](https://eventbridge-canon.netlify.app/)
57 |
58 | # Examples
59 |
60 | Here are some screenshots of examples of what EventBridge Canon looks like.
61 |
62 | ## Event Editor
63 |
64 |
65 |
66 | ## Saving Events
67 |
68 |
69 |
70 | ## Event Logs
71 |
72 |
73 |
74 | ## Editor with Collections
75 |
76 |
77 |
78 | # Contributing
79 |
80 | If you have any questions, features or issues please raise any issue or pull requests you like. I will try my best to get back to you.
81 |
82 | [license-badge]: https://img.shields.io/github/license/boyney123/eventbridge-canon.svg?color=yellow
83 | [license]: https://github.com/boyney123/eventbridge-canon/blob/main/LICENCE
84 | [prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square
85 | [prs]: http://makeapullrequest.com
86 | [github-watch-badge]: https://img.shields.io/github/watchers/boyney123/eventbridge-canon.svg?style=social
87 | [github-watch]: https://github.com/boyney123/eventbridge-canon/watchers
88 | [github-star-badge]: https://img.shields.io/github/stars/boyney123/eventbridge-canon.svg?style=social
89 | [github-star]: https://github.com/boyney123/eventbridge-canon/stargazers
90 |
91 | # Sponsor
92 |
93 | If you like this project, it would mean the world to me if you could buy me a drink to keep going!
94 |
95 | [Sponsor this project](https://github.com/sponsors/boyney123).
96 |
97 | # License
98 |
99 | MIT.
100 |
--------------------------------------------------------------------------------
/components/CollectionList.tsx:
--------------------------------------------------------------------------------
1 | /* This example requires Tailwind CSS v2.0+ */
2 | import { useState } from 'react';
3 | import { Disclosure } from '@headlessui/react';
4 | import Link from 'next/link';
5 | import toast from 'react-hot-toast';
6 |
7 | import { FolderIcon } from '@heroicons/react/outline';
8 | import useCollections from '@/hooks/useCollections';
9 | import CreateCollectionModal from '@/components/Modals/CreateCollectionModal';
10 |
11 | function classNames(...classes) {
12 | return classes.filter(Boolean).join(' ');
13 | }
14 |
15 | export default function CollectionList() {
16 | const [collections, { saveCollection }] = useCollections();
17 | const [showCollectionModal, setShowCollectionModal] = useState(false);
18 |
19 | const urlParts = window.location.href.split('/');
20 | const selectedEvent = urlParts[urlParts.length - 1];
21 |
22 | const handleCreate = async (collection) => {
23 | try {
24 | await saveCollection(collection);
25 | setShowCollectionModal(false);
26 | toast.success(`Successfully created collection "${collection.name}"!`);
27 | } catch (error) {
28 | toast.error('Failed to create colletion.');
29 | }
30 | };
31 |
32 | return (
33 |
34 |
setShowCollectionModal(false)} />
35 |
36 |
setShowCollectionModal(true)} className="mb-4 px-3 text-xs block w-full text-left text-pink-500 font-bold ">
37 | + Create Collection
38 |
39 | {collections.length > 0 && (
40 |
41 | {collections.map((collection) =>
42 | collection.events.length === 0 ? (
43 |
49 | ) : (
50 |
51 | {({ open }) => (
52 | <>
53 |
54 |
55 |
56 | {collection.name} ({collection.events.length})
57 |
58 |
59 |
60 |
61 |
62 |
63 | {collection.events.map((event) => (
64 |
72 | ))}
73 |
74 | >
75 | )}
76 |
77 | )
78 | )}
79 |
80 | )}
81 |
82 |
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/components/Editor.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { v4 as uuid } from 'uuid';
3 | import faker from 'faker';
4 |
5 | import MonacoEditor, { EditorProps } from '@monaco-editor/react';
6 |
7 | const Editor = (props: EditorProps) => {
8 | const onMount = (_, monaco) => {
9 | monaco.languages.registerCompletionItemProvider('json', {
10 | provideCompletionItems: function (model, position, ...args) {
11 | const word = model.getWordUntilPosition(position);
12 |
13 | const range = {
14 | startLineNumber: position.lineNumber,
15 | endLineNumber: position.lineNumber,
16 | startColumn: word.startColumn,
17 | endColumn: word.endColumn,
18 | };
19 |
20 | return {
21 | suggestions: [
22 | {
23 | label: 'uuid()',
24 | kind: monaco.languages.CompletionItemKind.Function,
25 | insertText: `"${uuid()}"`,
26 | range: range,
27 | },
28 | {
29 | label: 'randomName()',
30 | kind: monaco.languages.CompletionItemKind.Function,
31 | insertText: `"${faker.name.findName()}"`,
32 | range: range,
33 | },
34 | {
35 | label: 'email()',
36 | kind: monaco.languages.CompletionItemKind.Function,
37 | insertText: `"${faker.internet.email()}"`,
38 | range: range,
39 | },
40 | {
41 | label: 'image()',
42 | kind: monaco.languages.CompletionItemKind.Function,
43 | insertText: `"${faker.image.image()}"`,
44 | range: range,
45 | },
46 | {
47 | label: 'pastDate()',
48 | kind: monaco.languages.CompletionItemKind.Function,
49 | insertText: `"${faker.date.past()}"`,
50 | range: range,
51 | },
52 | {
53 | label: 'futureDate()',
54 | kind: monaco.languages.CompletionItemKind.Function,
55 | insertText: `"${faker.date.future()}"`,
56 | range: range,
57 | },
58 | ],
59 | };
60 | },
61 | });
62 | };
63 |
64 | const { options, ...otherProps } = props;
65 |
66 | return ;
67 | };
68 |
69 | export default Editor;
70 |
--------------------------------------------------------------------------------
/components/EventList.tsx:
--------------------------------------------------------------------------------
1 | /* This example requires Tailwind CSS v2.0+ */
2 | import { Disclosure } from '@headlessui/react';
3 | import Link from 'next/link';
4 |
5 | import { FolderIcon } from '@heroicons/react/outline';
6 | import useEvents from '@/hooks/useEvents';
7 |
8 | function classNames(...classes) {
9 | return classes.filter(Boolean).join(' ');
10 | }
11 |
12 | export default function EventList() {
13 | const [_, { getAllEventsAndEventSources }] = useEvents();
14 |
15 | const urlSearchParams = new URLSearchParams(window.location.search);
16 | const { source: selectedSource = '', detailType: selectedDetailType = '' } = Object.fromEntries(urlSearchParams.entries());
17 |
18 | const eventsBySource = getAllEventsAndEventSources();
19 |
20 | return (
21 |
22 |
23 | {Object.keys(eventsBySource).length > 0 && (
24 |
25 | {Object.keys(eventsBySource).map((source) => (
26 |
27 | {({ open }) => {
28 | const show = open || source == selectedSource;
29 | return (
30 | <>
31 |
32 |
33 |
34 | {source} ({eventsBySource[source].schemas.length})
35 |
36 |
37 |
38 |
39 |
40 | {show && (
41 |
42 | {eventsBySource[source].schemas.map((event) => (
43 |
53 | ))}
54 |
55 | )}
56 | >
57 | );
58 | }}
59 |
60 | ))}
61 |
62 | )}
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/components/EventViewer.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, Fragment } from 'react';
2 | import Head from 'next/head';
3 | import toast from 'react-hot-toast';
4 | import { useClipboard } from 'use-clipboard-copy';
5 | import { ChevronDownIcon, GlobeIcon, DocumentIcon, CollectionIcon } from '@heroicons/react/solid';
6 | import { DocumentIcon as DocumentIconOutline } from '@heroicons/react/outline';
7 | import { Menu, Transition } from '@headlessui/react';
8 | import { IAWSEventPayload, ILog } from '@/types/index';
9 |
10 | import Editor from '@/components/Editor';
11 |
12 | import SaveEventModal from '@/components/Modals/SaveEventModal';
13 | import ViewEventLog from '@/components/Modals/ViewEventLog';
14 | import PublishEventIntervalModal from '@/components/Modals/PublishEventIntervalModal';
15 |
16 | import useEvents from '@/hooks/useEvents';
17 | import useLogs from '@/hooks/useLogs';
18 |
19 | const tabs = [
20 | { name: 'Event', href: '#', current: true },
21 | // { name: 'Schema', href: '#', current: false },
22 | // { name: 'Targets', href: '#', current: false },
23 | ];
24 |
25 | function classNames(...classes) {
26 | return classes.filter(Boolean).join(' ');
27 | }
28 |
29 | interface IEventViewerProps {
30 | id?: string;
31 | name?: string;
32 | schemaName: string;
33 | description?: string;
34 | version: string;
35 | payload: IAWSEventPayload;
36 | source: string;
37 | eventBus: string;
38 | detailType: string;
39 | isEditingEvent?: boolean;
40 | collectionId?: string;
41 | }
42 |
43 | const EventViewer = ({ id, name, schemaName, description, version, payload, source, detailType, eventBus, isEditingEvent = false, collectionId }: IEventViewerProps) => {
44 | const [editorValue, setEditorValue] = useState(payload);
45 | const [interval, setCustomInterval] = useState(null);
46 |
47 | const clipboard = useClipboard();
48 |
49 | useEffect(() => {
50 | setEditorValue(payload);
51 | // eslint-disable-next-line react-hooks/exhaustive-deps
52 | }, [id, name, detailType]);
53 |
54 | const [showSaveEventModal, setShowEventModal] = useState(false);
55 | const [showPublishIntervalModal, setShowPublishIntervalModal] = useState(false);
56 | const [selectedLog, setSelectedLog] = useState();
57 | const [__, { getLogsForEvent }] = useLogs();
58 |
59 | const [_, { saveEvent, updateEvent, publishEvent }] = useEvents();
60 |
61 | const logs = getLogsForEvent(id, detailType);
62 |
63 | const sendEventToBus = async (payload) => {
64 | try {
65 | toast.promise(
66 | publishEvent({
67 | id,
68 | payload,
69 | }),
70 | {
71 | loading: 'Publishing event...',
72 | success: (
73 |
74 | Succesfully Published Event
75 | {schemaName}
76 |
77 | ),
78 | error: (
79 |
80 | Failed to publish event
81 | {schemaName}
82 |
83 | ),
84 | },
85 | {
86 | duration: 2000,
87 | icon: ,
88 | }
89 | );
90 | setSelectedLog(null);
91 | } catch (error) {
92 | toast.error('Failed to publish event');
93 | }
94 | };
95 |
96 | const handleResendEvent = async (log: ILog) => {
97 | const { payload } = log;
98 | await sendEventToBus(payload);
99 | setSelectedLog(null);
100 | };
101 |
102 | const handlePublishEvent = async () => {
103 | await sendEventToBus(editorValue);
104 | };
105 |
106 | const handleUpdateEvent = async () => {
107 | try {
108 | toast.promise(
109 | updateEvent({
110 | id,
111 | payload: editorValue,
112 | collectionId,
113 | }),
114 | {
115 | loading: 'Updating event...',
116 | success: (
117 |
118 | Updated Event
119 | {name}
120 |
121 | ),
122 | error: (
123 |
124 | Failed to update event
125 | {name}
126 |
127 | ),
128 | },
129 | {
130 | duration: 2000,
131 | }
132 | );
133 | } catch (error) {
134 | toast.error('Failed to update event');
135 | }
136 | };
137 |
138 | const handleSaveEvent = async (eventMetadata) => {
139 | try {
140 | const newEvent = await saveEvent({
141 | detailType,
142 | source,
143 | version,
144 | eventBus,
145 | schemaName,
146 | payload: editorValue,
147 | ...eventMetadata,
148 | });
149 | toast.success('Successfully saved event');
150 |
151 | window.location.href = `/collection/${newEvent.collectionId}/event/${newEvent.id}`;
152 |
153 | // TODO Fix this with state updates.
154 | // router.push(`/collection/${newEvent.collectionId}/event/${newEvent.id}`);
155 | } catch (error) {
156 | toast.error('Failed to create event');
157 | }
158 | };
159 |
160 | const handleCreateInterval = async ({ interval: intervalInSeconds }) => {
161 | setCustomInterval({
162 | value: intervalInSeconds,
163 | interval: setInterval(() => {
164 | handlePublishEvent();
165 | }, intervalInSeconds * 1000),
166 | });
167 |
168 | setShowPublishIntervalModal(false);
169 | };
170 |
171 | const stopPublishInterval = async () => {
172 | clearInterval(interval.interval);
173 | setCustomInterval(null);
174 | };
175 |
176 | const handleExportEvent = async () => {
177 | clipboard.copy(
178 | JSON.stringify(
179 | {
180 | detailType,
181 | source,
182 | version,
183 | eventBus,
184 | schemaName,
185 | payload: editorValue,
186 | descripton: description || '',
187 | name,
188 | },
189 | null,
190 | 4
191 | )
192 | );
193 | toast.success('Succesfully copied to clipboard');
194 | };
195 |
196 | return (
197 |
198 |
199 |
{name || detailType}
200 |
201 |
setShowEventModal(false)} />
202 | setSelectedLog(null)} onResend={handleResendEvent} />
203 | setShowPublishIntervalModal(null)} onCreate={handleCreateInterval} />
204 |
205 | <>
206 |
207 |
208 |
209 |
210 |
211 |
{name || schemaName}
212 | {description &&
{description} }
213 |
214 |
215 |
216 | {eventBus}
217 |
218 |
219 |
220 | {source}
221 |
222 |
223 |
224 | {detailType} (v{version})
225 |
226 |
227 |
228 |
229 |
230 |
231 | {
233 | interval ? stopPublishInterval() : handlePublishEvent();
234 | }}
235 | type="button"
236 | className="relative inline-flex items-center px-4 py-2 rounded-l-md border border-pink-300 bg-pink-600 text-sm font-medium text-white hover:bg-pink-500 focus:z-10 focus:outline-none focus:ring-1 focus:ring-pink-500 focus:border-pink-500"
237 | >
238 | {interval ? 'Stop Publishing' : 'Publish Event'}
239 |
240 |
241 | {({ open }) => (
242 | <>
243 |
244 | Open options
245 |
246 |
247 |
257 |
258 |
259 |
260 | setShowPublishIntervalModal(true)} className="text-gray-700 block px-4 py-2 text-sm">
261 | Publish on Interval
262 |
263 |
264 |
265 |
266 |
267 | >
268 | )}
269 |
270 |
271 |
272 | {
274 | if (isEditingEvent) {
275 | handleUpdateEvent();
276 | } else {
277 | setShowEventModal(true);
278 | }
279 | }}
280 | type="button"
281 | className="relative inline-flex items-center px-4 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-700 hover:bg-gray-50 focus:z-10 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
282 | >
283 | Save
284 |
285 |
286 | {({ open }) => (
287 | <>
288 |
289 | Open options
290 |
291 |
292 |
302 |
303 |
304 |
305 | setShowEventModal(true)} className="text-gray-700 block px-4 py-2 text-sm">
306 | Save As...
307 |
308 |
309 |
310 |
311 |
312 | handleExportEvent()} className="text-gray-700 block px-4 py-2 text-sm">
313 | Export Event
314 |
315 |
316 |
317 |
318 |
319 | >
320 | )}
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 | Select a tab
331 |
332 | tab.current).name}>
333 | {tabs.map((tab) => (
334 | {tab.name}
335 | ))}
336 |
337 |
338 |
349 |
350 | {interval && interval.value &&
Currently publishing event every {interval.value} seconds...
}
351 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 | {
362 | try {
363 | setEditorValue(JSON.parse(value));
364 | } catch (error) {}
365 | }}
366 | />
367 |
368 | {logs.length > 0 && (
369 | <>
370 | Event Logs
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 | AWS Event ID
380 |
381 |
382 | Status
383 |
384 |
385 | Created At
386 |
387 |
388 |
389 |
390 | {logs.reverse().map((log) => (
391 |
392 |
393 | setSelectedLog(log)}>
394 | {log.awsPublishEventId}
395 |
396 |
397 |
398 | Success
399 |
400 | {log.createdAt}
401 |
402 | ))}
403 |
404 |
405 |
406 |
407 |
408 |
409 | >
410 | )}
411 | {logs.length === 0 && (
412 |
413 |
Event Logs
414 |
No logs found. Start by publishing your event.
415 |
416 | )}
417 |
418 |
419 | >
420 |
421 | );
422 | };
423 |
424 | export default EventViewer;
425 |
--------------------------------------------------------------------------------
/components/Modals/CreateCollectionModal.tsx:
--------------------------------------------------------------------------------
1 | /* This example requires Tailwind CSS v2.0+ */
2 | import { Fragment, useRef, useState } from 'react';
3 | import { Dialog, Transition } from '@headlessui/react';
4 | import { IModalProps } from '@/types/index';
5 |
6 | export default function CreateCollectionModal({ isOpen = false, onCreate, onCancel = () => {} }: IModalProps) {
7 | const [collectionName, setCollectionName] = useState('');
8 | const [collectionDescription, setCollectionDescription] = useState('');
9 |
10 | const cancelButtonRef = useRef(null);
11 |
12 | const handleOnCreate = () => {
13 | onCreate({ name: collectionName, description: collectionDescription });
14 | };
15 |
16 | const handleOnCancel = () => {
17 | onCancel();
18 | };
19 |
20 | return (
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {/* This element is to trick the browser into centering the modal contents. */}
29 |
30 |
31 |
32 |
41 |
42 |
43 |
44 |
45 | New Collection
46 |
47 |
Store your events in collections.
48 |
49 |
50 |
51 | Name:
52 |
53 | setCollectionName(e.target.value)} />
54 |
55 |
56 |
57 | Description (optional):
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
73 | Create Collection
74 |
75 |
81 | Cancel
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | );
90 | }
91 |
--------------------------------------------------------------------------------
/components/Modals/ImportEventModal.tsx:
--------------------------------------------------------------------------------
1 | /* This example requires Tailwind CSS v2.0+ */
2 | import { Fragment, useRef, useState } from 'react';
3 | import { Dialog, Transition } from '@headlessui/react';
4 | import useEvents from '@/hooks/useEvents';
5 | import useCollections from '@/hooks/useCollections';
6 | import Editor from '@/components/Editor';
7 |
8 | import { IModalProps } from '@/types/index';
9 |
10 | export default function NewEventModal({ isOpen = false, onCreate, onCancel = () => {} }: IModalProps) {
11 | const [selectedCollection, setSelectedCollection] = useState('');
12 | const [event, setEvent] = useState({});
13 |
14 | const cancelButtonRef = useRef(null);
15 |
16 | const [collections] = useCollections();
17 |
18 | const handleOnCreate = async () => {
19 | onCreate({ ...event, collectionId: selectedCollection });
20 | };
21 |
22 | const handleOnCancel = () => {
23 | onCancel();
24 | };
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {/* This element is to trick the browser into centering the modal contents. */}
35 |
36 |
37 |
38 |
47 |
48 |
49 |
50 |
51 | Import Event
52 |
53 |
Paste your event and store it within a collection.
54 |
55 |
56 |
57 | Event:
58 |
59 | {
62 | try {
63 | setEvent(JSON.parse(value));
64 | } catch (error) {}
65 | }}
66 | />
67 |
68 |
69 |
70 | Collection:
71 |
72 | setSelectedCollection(e.target.value)}>
73 | Please Select
74 | {collections.map(({ id, name, events }) => (
75 |
76 | {name} ({events.length} Events)
77 |
78 | ))}
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
94 | Import Event
95 |
96 |
102 | Cancel
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | );
111 | }
112 |
--------------------------------------------------------------------------------
/components/Modals/NewEventModal.tsx:
--------------------------------------------------------------------------------
1 | /* This example requires Tailwind CSS v2.0+ */
2 | import { Fragment, useRef, useState } from 'react';
3 | import { Dialog, Transition } from '@headlessui/react';
4 | import useEvents from '@/hooks/useEvents';
5 | import { IModalProps } from '@/types/index';
6 |
7 | export default function NewEventModal({ isOpen = false, onCreate, onCancel = () => {} }: IModalProps) {
8 | const [selectedSource, setSelectedSource] = useState('');
9 | const [selectedDetailType, setSelectedDetailType] = useState('');
10 |
11 | const cancelButtonRef = useRef(null);
12 |
13 | const [_, { getAllEventsAndEventSources }] = useEvents();
14 | const sourcesAndEvents = getAllEventsAndEventSources();
15 |
16 | const listOfSources = Object.keys(sourcesAndEvents);
17 | const listOfDetailTypes = selectedSource ? sourcesAndEvents[selectedSource].schemas : [];
18 |
19 | const handleOnCreate = () => {
20 | onCreate({ source: selectedSource, detailType: selectedDetailType });
21 | };
22 |
23 | const handleOnCancel = () => {
24 | onCancel();
25 | };
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {/* This element is to trick the browser into centering the modal contents. */}
36 |
37 |
38 |
39 |
48 |
49 |
50 |
51 |
52 | New Event
53 |
54 |
Select your Source and Event
55 |
56 |
57 |
58 | Event Source:
59 |
60 | setSelectedSource(e.target.value)}>
61 | Please Select
62 | {listOfSources.map((source) => {
63 | return (
64 |
65 | {source}
66 |
67 | );
68 | })}
69 |
70 |
71 |
72 |
73 | Detail Type:
74 |
75 | setSelectedDetailType(e.target.value)}>
76 | Please Select
77 | {listOfDetailTypes.map((detailType) => {
78 | return (
79 |
80 | {detailType}
81 |
82 | );
83 | })}
84 |
85 |
86 |
87 |
88 |
89 |
90 |
98 | Create Event
99 |
100 |
106 | Cancel
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | );
115 | }
116 |
--------------------------------------------------------------------------------
/components/Modals/PublishEventIntervalModal.tsx:
--------------------------------------------------------------------------------
1 | /* This example requires Tailwind CSS v2.0+ */
2 | import { Fragment, useRef, useState, FunctionComponent } from 'react';
3 | import { Dialog, Transition } from '@headlessui/react';
4 | import useCollections from '@/hooks/useCollections';
5 | import { IModalProps } from '@/types/index';
6 |
7 | export const PublishEventIntervalModal = ({ isOpen = false, onCreate = () => {}, onCancel = () => {} }: IModalProps) => {
8 | const [event, setEvent] = useState({ interval: 5 });
9 |
10 | const [collections] = useCollections();
11 | const cancelButtonRef = useRef(null);
12 |
13 | const handleOnCreate = async () => {
14 | onCreate(event);
15 | };
16 |
17 | const handleOnCancel = () => {
18 | onCancel();
19 | };
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {/* This element is to trick the browser into centering the modal contents. */}
30 |
31 |
32 |
33 |
42 |
43 |
44 |
45 |
46 | Publish Interval
47 |
48 |
Keep publishing event on an interval (e.g every X seconds)
49 |
50 |
51 |
52 | Interval (in seconds):
53 |
54 | setEvent({ ...event, interval: e.target.value })} />
55 |
56 |
57 |
58 |
59 |
60 |
68 | Start Publishing
69 |
70 |
76 | Cancel
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | );
85 | };
86 |
87 | export default PublishEventIntervalModal;
88 |
--------------------------------------------------------------------------------
/components/Modals/SaveEventModal.tsx:
--------------------------------------------------------------------------------
1 | /* This example requires Tailwind CSS v2.0+ */
2 | import { Fragment, useRef, useState, FunctionComponent } from 'react';
3 | import { Dialog, Transition } from '@headlessui/react';
4 | import useCollections from '@/hooks/useCollections';
5 | import { IModalProps } from '@/types/index';
6 |
7 | export const SaveEventModal = ({ isOpen = false, onCreate = () => {}, onCancel = () => {} }: IModalProps) => {
8 | const [event, setEvent] = useState({ name: '', description: '', collectionId: '' });
9 |
10 | const [collections] = useCollections();
11 | const cancelButtonRef = useRef(null);
12 |
13 | const handleOnCreate = async () => {
14 | onCreate(event);
15 | };
16 |
17 | const handleOnCancel = () => {
18 | onCancel();
19 | };
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {/* This element is to trick the browser into centering the modal contents. */}
30 |
31 |
32 |
33 |
42 |
43 |
44 |
45 |
46 | Save Event
47 |
48 |
Save this event to a collection (a group of events).
49 |
50 |
51 |
52 | Name:
53 |
54 | setEvent({ ...event, name: e.target.value })} />
55 |
56 |
57 |
58 | Description (optional):
59 |
60 |
61 |
62 |
63 |
64 | Collection:
65 |
66 | setEvent({ ...event, collectionId: e.target.value })}>
67 | Please Select
68 | {collections.map(({ id, name, events }) => (
69 |
70 | {name} ({events.length} Events)
71 |
72 | ))}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
87 | Save Event
88 |
89 |
95 | Cancel
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | );
104 | };
105 |
106 | export default SaveEventModal;
107 |
--------------------------------------------------------------------------------
/components/Modals/ViewEventLog.tsx:
--------------------------------------------------------------------------------
1 | /* This example requires Tailwind CSS v2.0+ */
2 | import { Fragment, useRef, useState } from 'react';
3 | import { Dialog, Transition } from '@headlessui/react';
4 | import Editor from '@/components/Editor';
5 |
6 | import { IModalProps, ILog } from '@/types/index';
7 |
8 | interface IViewEventProps extends IModalProps {
9 | log?: ILog | null;
10 | onResend: (newEvent: ILog) => void;
11 | }
12 |
13 | export default function ViewEventModal({ isOpen = false, onResend, onCancel = () => {}, log }: IViewEventProps) {
14 | const cancelButtonRef = useRef(null);
15 |
16 | const handleResendEvent = async () => {
17 | onResend(log);
18 | };
19 |
20 | const handleOnCancel = () => {
21 | onCancel();
22 | };
23 |
24 | if (!log) return null;
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | {/* This element is to trick the browser into centering the modal contents. */}
35 |
36 |
37 |
38 |
47 |
48 |
49 |
50 |
51 | {log.awsPublishEventId}
52 |
53 |
This event was previously sent. You can see the payload below.
54 |
55 |
56 |
57 | Event:
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
71 | Resend Event
72 |
73 |
79 | Close
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | );
88 | }
89 |
--------------------------------------------------------------------------------
/components/SideBar.tsx:
--------------------------------------------------------------------------------
1 | /* This example requires Tailwind CSS v2.0+ */
2 | import { useState } from 'react';
3 |
4 | const tabs = [{ name: 'Events' }, { name: 'My Collections' }];
5 |
6 | function classNames(...classes) {
7 | return classes.filter(Boolean).join(' ');
8 | }
9 |
10 | export default function CollectionList({ children = (state: any) => {} }) {
11 | const [selectedTab, setSelectedTab] = useState('Events');
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | {tabs.map((tab) => (
20 | setSelectedTab(tab.name)}
23 | className={classNames(tab.name === selectedTab ? 'border-pink-500 text-pink-600' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300', 'w-1/2 text-center whitespace-nowrap py-4 border-b-2 font-medium text-sm')}
24 | >
25 | {tab.name}
26 |
27 | ))}
28 |
29 |
30 | {children({ selectedTab })}
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/components/TopNavigationBar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useRouter, NextRouter } from 'next/router';
3 | import NewEventModal from './Modals/NewEventModal';
4 | import ImportEventModal from './Modals/ImportEventModal';
5 |
6 | import useEvents from '@/hooks/useEvents';
7 |
8 | import Link from 'next/link';
9 | import { ICollectionEvent } from '../types';
10 | import toast from 'react-hot-toast';
11 |
12 | interface IHandleCreate {
13 | source: string;
14 | detailType: string;
15 | }
16 |
17 | const TopNavigationBar = () => {
18 | const router: NextRouter = useRouter();
19 | const [creatingNewEvent, setCreatingNewEvent] = useState(false);
20 | const [importingEvent, setImportingEvent] = useState(false);
21 |
22 | const [_, { saveEvent }] = useEvents();
23 |
24 | const handleCreate = ({ source, detailType }: IHandleCreate) => {
25 | router.push(`/events/create?source=${source}&detailType=${detailType}`);
26 | setCreatingNewEvent(false);
27 | };
28 |
29 | const handleImport = async (event: ICollectionEvent) => {
30 | const importedEvent = await saveEvent(event);
31 | toast.success('Succesfully imported event, loading event...');
32 | window.location.href = `/collection/${importedEvent.collectionId}/event/${importedEvent.id}`;
33 | };
34 |
35 | return (
36 |
37 |
setCreatingNewEvent(false)} onCreate={handleCreate} />
38 | setImportingEvent(false)} onCreate={handleImport} />
39 |
40 |
41 |
42 |
43 | EventBridge Canon
44 |
45 |
46 |
47 |
48 |
49 | setImportingEvent(true)}
52 | className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-transparent bg-gray-700 border-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-pink-500"
53 | >
54 | Import Event
55 |
56 | setCreatingNewEvent(true)}
59 | className="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-pink-600 hover:bg-pink-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-pink-500"
60 | >
61 | New Event
62 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default TopNavigationBar;
72 |
--------------------------------------------------------------------------------
/contexts/EventBridgeCanon.ts:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const EventBridgeCanonContext = React.createContext(null);
3 |
--------------------------------------------------------------------------------
/data/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/data/.gitkeep
--------------------------------------------------------------------------------
/hooks/useApplication.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { EventBridgeCanonContext } from '@/contexts/EventBridgeCanon';
3 | import { IApplicationData } from '@/types/index';
4 |
5 | interface IHookFunctions {
6 | fetchFromDB: () => Promise;
7 | }
8 |
9 | export const useApplication = (): [IApplicationData, IHookFunctions] => {
10 | const [applicationData = {}, setApplicationData] = useContext<[IApplicationData, any]>(EventBridgeCanonContext);
11 |
12 | const fetchFromDB = async (): Promise => {
13 | try {
14 | const response = await fetch('/api/data', {
15 | method: 'GET',
16 | });
17 |
18 | const data = await response.json();
19 |
20 | setApplicationData(data);
21 |
22 | return data;
23 | } catch (error) {
24 | throw Error(error);
25 | }
26 | };
27 |
28 | return [applicationData, { fetchFromDB }];
29 | };
30 |
31 | export default useApplication;
32 |
--------------------------------------------------------------------------------
/hooks/useCollections.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { EventBridgeCanonContext } from '@/contexts/EventBridgeCanon';
3 | import { IApplicationData, ICollection } from '@/types/index';
4 | import { post } from '@/utils/request';
5 |
6 | interface IHookFunctions {
7 | saveCollection: (collection: ICollection) => Promise;
8 | }
9 |
10 | export const useCollections = (): [ICollection[], IHookFunctions] => {
11 | // TODO: fix this context any
12 | const [applicationData = {}, setApplicationData] = useContext(EventBridgeCanonContext);
13 | const { collections }: IApplicationData = applicationData;
14 |
15 | const saveCollection = async (collection: ICollection): Promise => {
16 | try {
17 | const newCollection = await post('/api/collections', { body: { collection } });
18 |
19 | setApplicationData({
20 | ...applicationData,
21 | collections: collections.concat([newCollection]),
22 | });
23 |
24 | return newCollection;
25 | } catch (error) {
26 | console.log(error);
27 | }
28 | };
29 |
30 | return [collections, { saveCollection }];
31 | };
32 |
33 | export default useCollections;
34 |
--------------------------------------------------------------------------------
/hooks/useEvents.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { EventBridgeCanonContext } from '@/contexts/EventBridgeCanon';
3 | import { IApplicationData, ICollectionEvent, ILog, ISchema } from '@/types/index';
4 |
5 | interface IHookFunctions {
6 | updateEvent: (event) => Promise;
7 | publishEvent: (event) => Promise;
8 | saveEvent: (event) => Promise;
9 | getAllEventsAndEventSources: () => any; // TODO fix type
10 | }
11 |
12 | const useEvents = (): [ISchema[], IHookFunctions] => {
13 | // Fix any
14 | const [applicationData = {}, setApplicationData] = useContext<[IApplicationData, any]>(EventBridgeCanonContext);
15 |
16 | const { schemas, logs }: IApplicationData = applicationData;
17 |
18 | const updateEvent = async (customEvent): Promise => {
19 | const { schema, ...payload } = customEvent;
20 |
21 | try {
22 | const response = await fetch(`/api/collections/${customEvent.collectionId}/events`, {
23 | body: JSON.stringify({ event: payload }, null, 4),
24 | headers: {
25 | 'Content-Type': 'application/json',
26 | },
27 | method: 'PUT',
28 | });
29 | const event = await response.json();
30 |
31 | return event;
32 | } catch (error) {
33 | throw Error(error);
34 | }
35 | };
36 |
37 | const publishEvent = async (event): Promise => {
38 | try {
39 | const response = await fetch(`/api/events/publish`, {
40 | body: JSON.stringify(event),
41 | headers: {
42 | 'Content-Type': 'application/json',
43 | },
44 | method: 'POST',
45 | });
46 |
47 | const newLog = await response.json();
48 |
49 | setApplicationData({
50 | ...applicationData,
51 | logs: logs.concat([newLog]),
52 | });
53 |
54 | return newLog;
55 | } catch (error) {
56 | throw Error(error);
57 | }
58 | };
59 |
60 | const saveEvent = async (event): Promise => {
61 | try {
62 | const newEvent = await fetch(`/api/collections/${event.collectionId}/events`, {
63 | body: JSON.stringify({
64 | event,
65 | }),
66 | headers: {
67 | 'Content-Type': 'application/json',
68 | },
69 | method: 'POST',
70 | });
71 |
72 | const response = await newEvent.json();
73 | return response;
74 | } catch (error) {
75 | throw Error(error);
76 | }
77 | };
78 |
79 | const getAllEventsAndEventSources = () => {
80 | return (
81 | (schemas &&
82 | schemas.reduce((sources, { source, detailType }) => {
83 | sources[source] = sources[source] || { schemas: [] };
84 | sources[source].schemas.push(detailType);
85 | return sources;
86 | }, {})) ||
87 | []
88 | );
89 | };
90 |
91 | return [
92 | schemas,
93 | {
94 | saveEvent,
95 | publishEvent,
96 | updateEvent,
97 | getAllEventsAndEventSources,
98 | },
99 | ];
100 | };
101 |
102 | export default useEvents;
103 |
--------------------------------------------------------------------------------
/hooks/useLogs.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from 'react';
2 | import { EventBridgeCanonContext } from '@/contexts/EventBridgeCanon';
3 | import { IApplicationData, ILog } from '@/types/index';
4 |
5 | interface IHookFunctions {
6 | getLogsForEvent: (eventId: string, detailType?: string) => ILog[];
7 | }
8 |
9 | export const useLogs = (): [ILog[], IHookFunctions] => {
10 | const [applicationData] = useContext<[IApplicationData]>(EventBridgeCanonContext);
11 | const { logs } = applicationData || {};
12 |
13 | const getLogsForEvent = (eventId, detailType): ILog[] => {
14 | if (eventId) {
15 | return (logs && logs.filter((log) => log.eventId === eventId)) || [];
16 | } else {
17 | return (logs && logs.filter((log) => log?.payload?.DetailType === detailType && log?.eventId === undefined)) || [];
18 | }
19 | };
20 |
21 | return [logs, { getLogsForEvent }];
22 | };
23 | export default useLogs;
24 |
--------------------------------------------------------------------------------
/images/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/images/architecture.png
--------------------------------------------------------------------------------
/images/custom-event.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/images/custom-event.png
--------------------------------------------------------------------------------
/images/editor-and-collections.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/images/editor-and-collections.png
--------------------------------------------------------------------------------
/images/editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/images/editor.png
--------------------------------------------------------------------------------
/images/full-app-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/images/full-app-screenshot.png
--------------------------------------------------------------------------------
/images/logs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/images/logs.png
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/components/*": ["components/*"],
6 | "@/data/*": ["data/*"],
7 | "@/utils/*": ["utils/*"],
8 | "@/contexts/*": ["contexts/*"],
9 | "@/hooks/*": ["hooks/*"],
10 | "@/lib/*": ["lib/*"],
11 | "@/styles/*": ["styles/*"]
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lib/aws.js:
--------------------------------------------------------------------------------
1 | const { EventBridge } = require('@aws-sdk/client-eventbridge');
2 | const { Schemas } = require('@aws-sdk/client-schemas');
3 |
4 | const schemas = new Schemas();
5 | const eventbridge = new EventBridge();
6 |
7 | export const getTargetsForEventsOnEventBridge = async (eventBusName) => {
8 | const targetsForEvents = await eventbridge.listRules({ EventBusName: eventBusName });
9 | return buildTargets(targetsForEvents.Rules);
10 | };
11 |
12 | export const getAllSchemas = async (registryName) => {
13 | const { Schemas = [] } = await schemas.listSchemas({ RegistryName: registryName });
14 | return Schemas;
15 | };
16 |
17 | export const getJSONSchemaForSchemaName = (schemaName) => {
18 | return schemas.exportSchema({
19 | RegistryName: process.env.SCHEMA_REGISTRY_NAME,
20 | SchemaName: schemaName,
21 | Type: 'JSONSchemaDraft4',
22 | });
23 | };
24 |
25 | export const getAllSchemasAsJSONSchema = async (registryName) => {
26 | const { Schemas = [] } = await schemas.listSchemas({ RegistryName: registryName });
27 |
28 | return Schemas.map(async (schema) => {
29 | return schemas.exportSchema({
30 | RegistryName: registryName,
31 | SchemaName: schema.SchemaName,
32 | Type: 'JSONSchemaDraft4',
33 | });
34 | });
35 | };
36 | export const hydrateSchemasWithAdditionalOpenAPIData = async (registryName, schemaList) => {
37 | return schemaList.map(async (rawSchema) => {
38 | const schema = buildSchema(rawSchema);
39 |
40 | // get the schema as open API too, as its has more metadata we might find useful.
41 | const openAPISchema = await schemas.describeSchema({
42 | RegistryName: registryName,
43 | SchemaName: schema.SchemaName,
44 | });
45 | const schemaAsOpenAPI = buildSchema(openAPISchema);
46 |
47 | const { LastModified, SchemaArn, SchemaVersion, Tags, VersionCreatedDate } = schemaAsOpenAPI;
48 |
49 | return {
50 | ...schema,
51 | LastModified,
52 | SchemaArn,
53 | SchemaVersion,
54 | Tags,
55 | VersionCreatedDate,
56 | };
57 | });
58 | };
59 |
60 | export const buildSchema = (rawSchema) => {
61 | return { ...rawSchema, Content: JSON.parse(rawSchema.Content) };
62 | };
63 |
64 | export const buildTargets = (busRules) => {
65 | return busRules.reduce((rules, rule) => {
66 | const eventPattern = JSON.parse(rule.EventPattern);
67 | const detailType = eventPattern['detail-type'] || [];
68 | detailType.forEach((detail) => {
69 | if (!rules[detail]) {
70 | rules[detail] = { rules: [] };
71 | }
72 | rules[detail].rules.push(rule.Name);
73 | });
74 | return rules;
75 | }, {});
76 | };
77 |
78 | export const putEvent = (event) => {
79 | return eventbridge.putEvents({
80 | Entries: [event],
81 | });
82 | };
83 |
--------------------------------------------------------------------------------
/lib/db.js:
--------------------------------------------------------------------------------
1 | // TODO: Get populate script to populate the initial datbase stuff.
2 | import initialData from '../data/database.json';
3 | import { Low, JSONFile } from 'lowdb';
4 |
5 | const adapter = new JSONFile('./data/database.json');
6 | const db = new Low(adapter);
7 |
8 | db.data = initialData || {
9 | collections: [],
10 | customEvents: [],
11 | logs: [],
12 | ...data,
13 | };
14 |
15 | export const getAllEventsBySource = (source) => {
16 | return db.data.events.filter((event) => {
17 | return event.source === source;
18 | });
19 | };
20 |
21 | // tODO :REMOVE THIS
22 | export const getEventByName = (name) => {
23 | return db.data.events.find((event) => {
24 | return event.detailType === name;
25 | });
26 | };
27 |
28 | export const getSchemaByDetailType = (detailType) => {
29 | return db.data.schemas.find((schema) => {
30 | return schema.detailType === detailType;
31 | });
32 | };
33 |
34 | export const getEventFromCollection = (collectionId, eventId) => {
35 | const collection = db.data.collections.find(({ id }) => id === collectionId);
36 | return collection.events.find(({ id }) => id === eventId);
37 | };
38 |
39 | /**
40 | * EVENTS
41 | */
42 |
43 | export const createEvent = async (customEvent) => {
44 | const { collectionId, ...event } = customEvent;
45 |
46 | const collections = db.data.collections.map((collection) => {
47 | if (collection.id === collectionId) {
48 | return {
49 | ...collection,
50 | events: collection.events.concat([event]),
51 | };
52 | }
53 | return collection;
54 | });
55 |
56 | db.data.collections = collections;
57 |
58 | await db.write();
59 | };
60 |
61 | export const updateEvent = async (collectionId, customEvent) => {
62 | // TODO: Fix this, update things better.
63 | const collections = db.data.collections.map((collection) => {
64 | if (collection.id === collectionId) {
65 | return {
66 | ...collection,
67 | events: collection.events.map((event) => {
68 | if (event.id === customEvent.id) {
69 | return {
70 | ...event,
71 | ...customEvent,
72 | };
73 | }
74 | return event;
75 | }),
76 | };
77 | }
78 | return collection;
79 | });
80 |
81 | db.data.collections = collections;
82 |
83 | await db.write();
84 | };
85 |
86 | export const saveCollection = async (collection) => {
87 | db.data.collections.push(collection);
88 | await db.write();
89 | };
90 |
91 | export const saveEventLog = async (logEvent) => {
92 | db.data.logs.push(logEvent);
93 | await db.write();
94 | };
95 |
96 | export default db;
97 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const withTM = require('next-transpile-modules')(['lowdb']);
2 |
3 | const path = require('path');
4 |
5 | module.exports = withTM({
6 | reactStrictMode: true,
7 | devServer: {
8 | watchOptions: {
9 | ignored: [path.resolve(__dirname, 'data')],
10 | },
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eventbridge-canon",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "populate": "babel-node scripts/populate.js"
11 | },
12 | "dependencies": {
13 | "@aws-sdk/client-eventbridge": "^3.21.0",
14 | "@aws-sdk/client-schemas": "^3.21.0",
15 | "@headlessui/react": "^1.3.0",
16 | "@heroicons/react": "^1.0.1",
17 | "@monaco-editor/react": "^4.2.1",
18 | "@tailwindcss/forms": "^0.3.3",
19 | "chalk": "^4.1.2",
20 | "faker": "^5.5.3",
21 | "json-schema-faker": "^0.5.0-rcv.38",
22 | "lowdb": "^2.1.0",
23 | "next": "11.0.1",
24 | "react": "17.0.2",
25 | "react-dom": "17.0.2",
26 | "react-hot-toast": "^2.1.0",
27 | "use-clipboard-copy": "^0.2.0",
28 | "uuid": "^8.3.2"
29 | },
30 | "devDependencies": {
31 | "@babel/core": "^7.14.6",
32 | "@babel/node": "^7.14.7",
33 | "@babel/preset-env": "^7.14.4",
34 | "@types/react": "^17.0.16",
35 | "autoprefixer": "^10.2.6",
36 | "dotenv": "^10.0.0",
37 | "eslint": "7.30.0",
38 | "eslint-config-next": "11.0.1",
39 | "next-transpile-modules": "^8.0.0",
40 | "postcss": "^8.3.5",
41 | "tailwindcss": "^2.2.4",
42 | "typescript": "^4.3.5"
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import Head from 'next/head';
3 |
4 | import dynamic from 'next/dynamic';
5 | const Toaster = dynamic(() => import('react-hot-toast').then((mod) => mod.Toaster), { ssr: false });
6 |
7 | import TopNavigationBar from '@/components/TopNavigationBar';
8 | import SideBar from '@/components/SideBar';
9 | import CollectionList from '@/components/CollectionList';
10 | import EventList from '@/components/EventList';
11 |
12 | import { EventBridgeCanonContext } from '@/contexts/EventBridgeCanon';
13 | import useApplication from '@/hooks/useApplication';
14 |
15 | import '../styles/globals.css';
16 |
17 | const App = ({ children }) => {
18 | const [data, { fetchFromDB }] = useApplication();
19 |
20 | useEffect(() => {
21 | const init = async () => {
22 | await fetchFromDB();
23 | };
24 | init();
25 | // eslint-disable-next-line react-hooks/exhaustive-deps
26 | }, []);
27 |
28 | if (!data.schemas) return null;
29 |
30 | return (
31 | <>
32 |
33 | EventBridge Canon
34 |
35 |
36 |
37 |
38 | {({ selectedTab }) => {
39 | return (
40 | <>
41 | {selectedTab === 'Events' && }
42 | {selectedTab === 'My Collections' && }
43 | >
44 | );
45 | }}
46 |
47 | {children}
48 |
49 |
50 | >
51 | );
52 | };
53 |
54 | function EventBridgeCanonApp({ Component, pageProps }) {
55 | const [applicationData, setApplicationData] = useState();
56 |
57 | return (
58 |
59 |
60 |
61 |
62 |
63 | );
64 | }
65 |
66 | export default EventBridgeCanonApp;
67 |
--------------------------------------------------------------------------------
/pages/api/collections.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from 'next';
2 | import { saveCollection } from '@/lib/db';
3 | import { v4 as uuid } from 'uuid';
4 | import { ICollection } from '@/types/index';
5 |
6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
7 | if (req.method === 'POST') {
8 | const { collection } = req.body;
9 |
10 | const newCollection = {
11 | ...collection,
12 | events: [],
13 | id: uuid(),
14 | };
15 |
16 | await saveCollection(newCollection);
17 |
18 | return res.status(201).json(newCollection);
19 | } else {
20 | return res.status(400);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/pages/api/collections/[collectionId]/events.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from 'next';
2 | import { createEvent, updateEvent } from '@/lib/db';
3 | import { v4 as uuid } from 'uuid';
4 | import { ICollectionEvent } from '@/types/index';
5 |
6 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
7 | const { event } = req.body;
8 | const { collectionId } = req.query;
9 |
10 | if (!collectionId) {
11 | return res.status(400);
12 | }
13 |
14 | switch (req.method) {
15 | case 'POST':
16 | const newEvent = { ...event, id: uuid(), collectionId };
17 | await createEvent(newEvent);
18 | return res.status(201).json(newEvent);
19 | case 'PUT':
20 | await updateEvent(collectionId, event);
21 | return res.status(201).json(event);
22 | default:
23 | return res.status(400);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/pages/api/data.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from 'next';
2 | import db from '@/lib/db';
3 | import { IApplicationData } from '@/types/index';
4 |
5 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
6 | if (req.method === 'GET') {
7 | return res.status(201).json(db.data);
8 | } else {
9 | return res.status(400);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/pages/api/events/publish.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from 'next';
2 |
3 | import { saveEventLog } from '@/lib/db';
4 | import { putEvent } from '@/lib/aws';
5 | import { v4 as uuid } from 'uuid';
6 | import { ILog } from '@/types/index';
7 |
8 | export default async function handler(req: NextApiRequest, res: NextApiResponse) {
9 | if (req.method === 'POST') {
10 | const { payload, id } = req.body;
11 |
12 | const { Entries = [] } = await putEvent({
13 | ...payload,
14 | Time: new Date(payload.Time),
15 | Detail: JSON.stringify(payload.Detail),
16 | });
17 |
18 | const awsPublishEventId = Entries[0]?.EventId;
19 |
20 | const log = {
21 | awsPublishEventId,
22 | eventId: id,
23 | createdAt: new Date(),
24 | payload,
25 | id: uuid(),
26 | };
27 |
28 | await saveEventLog(log);
29 |
30 | return res.status(201).json(log);
31 | } else {
32 | return res.status(400);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/pages/collection/[collectionId]/event/[eventId].tsx:
--------------------------------------------------------------------------------
1 | import { GetServerSideProps } from 'next';
2 | import { getEventFromCollection } from '@/lib/db';
3 | import EventViewer from '@/components/EventViewer';
4 |
5 | const EventSources = ({ collectionEvent, collectionId }) => {
6 | return ;
7 | };
8 |
9 | export const getServerSideProps: GetServerSideProps = async (req) => {
10 | const collectionId = req.params.collectionId;
11 | const eventId = req.params.eventId;
12 |
13 | const collectionEvent = getEventFromCollection(collectionId, eventId);
14 |
15 | return {
16 | props: {
17 | collectionId,
18 | collectionEvent,
19 | },
20 | };
21 | };
22 |
23 | export default EventSources;
24 |
--------------------------------------------------------------------------------
/pages/events/create.tsx:
--------------------------------------------------------------------------------
1 | import { GetServerSideProps } from 'next';
2 | import { generate } from 'json-schema-faker';
3 |
4 | import EventViewer from '@/components/EventViewer';
5 | import { clearValues } from '@/utils/clear-values';
6 |
7 | import { getSchemaByDetailType } from '@/lib/db';
8 | import { getJSONSchemaForSchemaName } from '@/lib/aws';
9 |
10 | const EventSources = ({ eventBus, schema }) => {
11 | const now = new Date();
12 |
13 | const { source, detailType, jsonSchema, version, schemaName } = schema;
14 |
15 | return (
16 |
31 | );
32 | };
33 |
34 | export const getServerSideProps: GetServerSideProps = async (req) => {
35 | const detailType = req.query.detailType;
36 |
37 | const schema = await getSchemaByDetailType(detailType);
38 |
39 | const { Content: JSONSchema, SchemaVersion } = await getJSONSchemaForSchemaName(schema.schemaName);
40 |
41 | return {
42 | props: {
43 | eventBus: process.env.EVENT_BUS_NAME,
44 | schema: {
45 | ...schema,
46 | version: SchemaVersion,
47 | jsonSchema: JSON.parse(JSONSchema),
48 | },
49 | },
50 | };
51 | };
52 |
53 | export default EventSources;
54 |
--------------------------------------------------------------------------------
/pages/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useRouter } from 'next/router';
3 | import { PlusIcon } from '@heroicons/react/solid';
4 | import { DocumentIcon } from '@heroicons/react/outline';
5 | import NewEventModal from '@/components/Modals/NewEventModal';
6 |
7 | const EventSources = () => {
8 | const router = useRouter();
9 |
10 | const [creatingNewEvent, setCreatingNewEvent] = useState(false);
11 |
12 | const hasProjects = false;
13 |
14 | const handleCreate = ({ source, detailType }) => {
15 | router.push(`/events/create?source=${source}&detailType=${detailType}`);
16 | };
17 |
18 | return (
19 |
20 |
setCreatingNewEvent(false)} onCreate={handleCreate} />
21 |
22 | {!hasProjects && (
23 |
24 |
25 |
EventBridge Canon
26 |
Publish, Save and Share AWS EventBridge Events
27 |
28 |
setCreatingNewEvent(true)}
30 | type="button"
31 | className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-pink-600 hover:bg-pink-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500"
32 | >
33 |
34 | Create Event
35 |
36 |
43 |
44 | Read Docs
45 |
46 |
47 |
48 |
49 | )}
50 | {hasProjects && <>Projects Bro>}
51 |
52 | );
53 | };
54 |
55 | export async function getServerSideProps(req) {
56 | return {
57 | props: {
58 | eventBus: process.env.EVENT_BUS_NAME,
59 | },
60 | };
61 | }
62 |
63 | export default EventSources;
64 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/canon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/eventbridge-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo-full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boyney123/eventbridge-canon/5d6cf710975bfb8f12e81b8a443359622763e225/public/logo-full.png
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/scripts/populate.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 | import fs from 'fs';
3 | import chalk from 'chalk';
4 | require('dotenv').config({ path: path.resolve(process.cwd(), '.env') });
5 |
6 | import { getAllSchemas } from '../lib/aws';
7 |
8 | const log = console.log;
9 |
10 | const REGSITRY_NAME = process.env.SCHEMA_REGISTRY_NAME;
11 | const EVENT_BUS_NAME = process.env.EVENT_BUS_NAME;
12 |
13 | const populate = async () => {
14 | log(chalk.green(`[1/2] - Fetching all schemas for event bus: ${EVENT_BUS_NAME}...`));
15 |
16 | const getAllSchemasForRegistry = await getAllSchemas(REGSITRY_NAME);
17 |
18 | log(chalk.green(`[2/2] - Populating local db with schemas...`));
19 |
20 | const schemasForDB = getAllSchemasForRegistry.map((schema) => {
21 | return {
22 | source: schema.SchemaName.split('@')[0],
23 | detailType: schema.SchemaName.split('@')[1],
24 | schemaName: schema.SchemaName,
25 | };
26 | });
27 |
28 | const datbaseDIR = path.join(__dirname, '../data/database.json');
29 |
30 | let fileData = {
31 | eventBusName: EVENT_BUS_NAME,
32 | schemas: schemasForDB,
33 | };
34 |
35 | if (fs.existsSync(datbaseDIR)) {
36 | const data = fs.readFileSync(datbaseDIR);
37 | const existingDB = JSON.parse(data.toString());
38 | fileData = { ...existingDB, ...fileData };
39 | } else {
40 | fileData = {
41 | ...fileData,
42 | collections: [],
43 | logs: [],
44 | };
45 | }
46 |
47 | fs.writeFileSync(datbaseDIR, JSON.stringify(fileData, null, 4));
48 |
49 | log(
50 | `
51 | Finished populating your local database with your schemas!
52 |
53 | EventBus: ${chalk.blue(EVENT_BUS_NAME)}
54 |
55 | Next run the command below to start EventBridge Canon
56 |
57 | ${chalk.blue(`npm run dev`)}
58 | `
59 | );
60 | };
61 |
62 | populate();
63 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
3 | darkMode: false, // or 'media' or 'class'
4 | theme: {
5 | extend: {},
6 | },
7 | variants: {
8 | extend: {},
9 | },
10 | plugins: [require('@tailwindcss/forms')],
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "baseUrl": ".",
17 | "paths": {
18 | "@/components/*": ["components/*"],
19 | "@/hooks/*": ["hooks/*"],
20 | "@/types/*": ["types/*"],
21 | "@/contexts/*": ["contexts/*"],
22 | "@/data/*": ["data/*"],
23 | "@/lib/*": ["lib/*"],
24 | "@/utils/*": ["utils/*"],
25 | "@/styles/*": ["styles/*"]
26 | }
27 | },
28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "lib/aws.js"],
29 | "exclude": ["node_modules"]
30 | }
31 |
--------------------------------------------------------------------------------
/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface IApplicationData {
2 | logs?: [ILog];
3 | collections?: [ICollection];
4 | schemas?: [ISchema];
5 | }
6 |
7 | export interface ISchema {
8 | source: string;
9 | detailType: string;
10 | schemaName: string;
11 | }
12 |
13 | export interface IAWSEventPayload {
14 | Time: string;
15 | Source: string;
16 | Resources: [string];
17 | DetailType: string;
18 | Detail: any;
19 | EventBusName: string;
20 | }
21 |
22 | export interface ICollectionEvent {
23 | id: string;
24 | collectionId: string;
25 | name: string;
26 | description?: string;
27 | detailType: string;
28 | source: string;
29 | version: string;
30 | eventBusName: string;
31 | schemaName: string;
32 | payload: IAWSEventPayload;
33 | }
34 |
35 | export interface ICollection {
36 | id: string;
37 | name: string;
38 | description?: string;
39 | events?: [ICollectionEvent?];
40 | }
41 |
42 | export interface ILog {
43 | id: string;
44 | awsPublishEventId: string;
45 | eventId: string;
46 | createdAt: string;
47 | payload: IAWSEventPayload;
48 | }
49 |
50 | export interface IModalProps {
51 | onCreate?: (newEvent: any) => void;
52 | onCancel: () => void;
53 | isOpen: boolean;
54 | }
55 |
--------------------------------------------------------------------------------
/utils/clear-values.ts:
--------------------------------------------------------------------------------
1 | export const clearValues = (obj) => {
2 | return Object.keys(obj).reduce((clearedValues, key) => {
3 | if (typeof obj[key] === 'object' && obj[key] !== null) {
4 | clearedValues[key] = clearValues(obj[key]);
5 | } else {
6 | clearedValues[key] = '';
7 | }
8 | return clearedValues;
9 | }, {});
10 | };
11 |
--------------------------------------------------------------------------------
/utils/request.ts:
--------------------------------------------------------------------------------
1 | export const post = async (url: string, input: { body: any }) => {
2 | const response = await fetch(url, {
3 | body: JSON.stringify(input.body),
4 | headers: {
5 | 'Content-Type': 'application/json',
6 | },
7 | method: 'POST',
8 | });
9 |
10 | return response.json();
11 | };
12 |
--------------------------------------------------------------------------------