├── 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 | header 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 | header 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 | header 65 | 66 | ## Saving Events 67 | 68 | header 69 | 70 | ## Event Logs 71 | 72 | header 73 | 74 | ## Editor with Collections 75 | 76 | header 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 | 39 | {collections.length > 0 && ( 40 | 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 | 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 |
218 |
219 |
222 |
223 |
226 |
227 |
228 | 229 |
230 | 231 | 240 | 241 | {({ open }) => ( 242 | <> 243 | 244 | Open options 245 | 247 | 257 | 258 |
259 | 260 | 263 | 264 |
265 |
266 |
267 | 268 | )} 269 |
270 |
271 | 272 | 285 | 286 | {({ open }) => ( 287 | <> 288 | 289 | Open options 290 | 292 | 302 | 303 |
304 | 305 | 308 | 309 |
310 |
311 | 312 | 315 | 316 |
317 |
318 |
319 | 320 | )} 321 |
322 |
323 |
324 |
325 | 326 |
327 |
328 |
329 | 332 | 337 |
338 |
339 |
340 | 347 |
348 |
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 | 381 | 384 | 387 | 388 | 389 | 390 | {logs.reverse().map((log) => ( 391 | 392 | 397 | 400 | 401 | 402 | ))} 403 | 404 |
379 | AWS Event ID 380 | 382 | Status 383 | 385 | Created At 386 |
393 | 396 | 398 | Success 399 | {log.createdAt}
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 | 32 | 41 |
42 |
43 |
44 | 45 | New Collection 46 | 47 |

Store your events in collections.

48 |
49 |
50 | 53 | setCollectionName(e.target.value)} /> 54 |
55 |
56 | 59 | 60 |
61 |
62 |
63 |
64 |
65 | 75 | 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 | 38 | 47 |
48 |
49 |
50 | 51 | Import Event 52 | 53 |

Paste your event and store it within a collection.

54 |
55 |
56 | 59 | { 62 | try { 63 | setEvent(JSON.parse(value)); 64 | } catch (error) {} 65 | }} 66 | /> 67 |
68 |
69 | 72 | 80 |
81 |
82 |
83 |
84 | 85 |
86 | 96 | 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 | 39 | 48 |
49 |
50 |
51 | 52 | New Event 53 | 54 |

Select your Source and Event

55 |
56 |
57 | 60 | 70 |
71 |
72 | 75 | 85 |
86 |
87 |
88 |
89 |
90 | 100 | 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 | 33 | 42 |
43 |
44 |
45 | 46 | Publish Interval 47 | 48 |

Keep publishing event on an interval (e.g every X seconds)

49 |
50 |
51 | 54 | setEvent({ ...event, interval: e.target.value })} /> 55 |
56 |
57 |
58 |
59 |
60 | 70 | 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 | 33 | 42 |
43 |
44 |
45 | 46 | Save Event 47 | 48 |

Save this event to a collection (a group of events).

49 |
50 |
51 | 54 | setEvent({ ...event, name: e.target.value })} /> 55 |
56 |
57 | 60 | 61 |
62 |
63 | 66 | 74 |
75 |
76 |
77 |
78 |
79 | 89 | 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 | 38 | 47 |
48 |
49 |
50 | 51 | {log.awsPublishEventId} 52 | 53 |

This event was previously sent. You can see the payload below.

54 |
55 |
56 | 59 | 60 |
61 |
62 |
63 |
64 | 65 |
66 | 73 | 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 | 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 | 56 | 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 | 36 | 43 | 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 | --------------------------------------------------------------------------------