├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── __tests__ ├── compositionsaveapi.spec.tsx └── flowname.spec.tsx ├── components ├── Execution │ ├── CloudProvidersDropdown.tsx │ ├── CompositionResult.tsx │ ├── Execution.tsx │ ├── LoginDeploy.tsx │ ├── PlatformSpecific.tsx │ └── UserInput.tsx ├── FlowChart-dynamic.tsx ├── FlowChart.tsx ├── FlowName.tsx ├── FlowStructureButton.tsx ├── FuncEditor.tsx ├── FunctionInventory.tsx ├── FunctionStructureButtons.tsx ├── Nav.tsx └── NavBar.tsx ├── data ├── db.ts ├── flowStructures.json └── users │ ├── compositions │ ├── demo-agnostic.json │ ├── demo.js │ └── demo.json │ └── functions.json ├── decl.d.ts ├── enzyme.js ├── jest.config.js ├── jest.tsconfig.json ├── next-env.d.ts ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── api │ ├── auth │ │ └── [...nextauth].ts │ ├── composition │ │ └── agnosticsave │ │ │ └── [compositionname].ts │ ├── functions │ │ ├── read-functions.js │ │ └── upsert-function.js │ ├── hello.js │ └── ibm │ │ ├── convert │ │ └── [compositionName].js │ │ ├── deploy │ │ └── [compositionName].js │ │ ├── invoke │ │ ├── [compositionName].js │ │ └── detailed │ │ │ └── [compositionName].js │ │ └── login.js ├── execution │ └── index.tsx └── index.tsx ├── public ├── favicon.ico └── vercel.svg ├── sonar-project.properties ├── store ├── reducers │ ├── canvasReducer.ts │ ├── editorReducer.ts │ ├── executionReducer.ts │ ├── functionsReducer.ts │ └── sequenceReducer.ts └── store.ts ├── styles ├── globals.css └── nav.module.css └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules/* 2 | **/out/* 3 | **/.next/* 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["@typescript-eslint"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:react/recommended", 7 | "plugin:@typescript-eslint/recommended" 8 | // "prettier", 9 | // "prettier/@typescript-eslint" 10 | ], 11 | "settings": { 12 | "react": { 13 | "version": "detect" 14 | } 15 | }, 16 | "env": { 17 | "es6": true, 18 | "browser": true, 19 | "jest": true, 20 | "node": true 21 | }, 22 | "rules": { 23 | "react/react-in-jsx-scope": 0, 24 | "react/display-name": 0, 25 | "react/prop-types": 0, 26 | "@typescript-eslint/explicit-function-return-type": 0, 27 | "@typescript-eslint/explicit-member-accessibility": 0, 28 | "@typescript-eslint/indent": 0, 29 | "@typescript-eslint/member-delimiter-style": 0, 30 | "@typescript-eslint/no-explicit-any": 0, 31 | "@typescript-eslint/no-var-requires": 0, 32 | "@typescript-eslint/no-use-before-define": 0, 33 | "@typescript-eslint/no-unused-vars": [ 34 | 2, 35 | { 36 | "argsIgnorePattern": "^_" 37 | } 38 | ], 39 | "no-console": [ 40 | 2, 41 | { 42 | "allow": ["warn", "error"] 43 | } 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # next.js 10 | /.next/ 11 | /out/ 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | 20 | # debug 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # local env files 26 | .env.local 27 | .env.development.local 28 | .env.test.local 29 | .env.production.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # cloud credentials 35 | /data/users/userconfig.json 36 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | yarn.lock 4 | package-lock.json 5 | public 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | addons: 5 | sonarcloud: 6 | organization: 'jpascas' # the key of the org you chose at step #3 7 | token: '$SONAR_TOKEN' # encrypted value of your token 8 | #after_success: 9 | # - npm build 10 | 11 | script: 12 | # other script steps might be done before running the actual analysis 13 | - npm build 14 | - npm run test 15 | - sonar-scanner 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 FaaSCompose 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FaaSCompose 2 | Graphical User Interface for Composing FaaS Workflows 3 | 4 | ## Background 5 | There are a variety of Function as a Service (FaaS) providers in the marketplace, including IBM Cloud Functions, Amazon/AWS Lambda, Azure Functions, and Google Cloud Functions. These providers issue functions in a proprietary manner, using their own methods and syntax for creating FaaS workflows. To work with each provider, you will need to write those functions with specific signatures in order for them to be readable by or usable on each of the respective platforms. Each provider’s implementation signature is slightly different from one another. This is also true with the composition and orchestration of those functions on larger workflows. Each provider has its own way to build and represent those workflows. This creates vendor lock-in. Therefore, a set of functions and a composition built for one provider will not work on a different platform. 6 | 7 | FaaSCompose aims to solve this problem by allowing the creation of functions and the associated workflows in an agnostic manner for subsequent conversion and execution on multiple cloud FaaS Providers. So far, the only cloud provider supported is IBM Cloud Functions, but in the future, we would like to include more options. IBM currently does not provide a GUI for composing workflows, which is why we chose to work on this provider first. 8 | 9 | ## How to Use 10 | 11 | ## Roadmap 12 | 13 | ## Contributing 14 | 15 | ## License 16 | -------------------------------------------------------------------------------- /__tests__/compositionsaveapi.spec.tsx: -------------------------------------------------------------------------------- 1 | import { createMocks } from 'node-mocks-http'; 2 | import handleSave from '../pages/api/composition/agnosticsave/[compositionname]'; 3 | 4 | jest.mock('next-auth/client', () => ({ 5 | getSession: jest.fn(() => ({ user: { name: 'user1' } })), 6 | })); 7 | 8 | jest.mock('../data/db', () => ({ 9 | task: jest.fn(), 10 | })); 11 | 12 | import db from '../data/db'; 13 | import { getSession } from 'next-auth/client'; 14 | 15 | describe('/api/composition/agnosticsave/[compositionName]', () => { 16 | beforeEach(() => { 17 | jest.resetModules(); 18 | jest.resetAllMocks(); 19 | }); 20 | it('returns status code 500 if there is no body', async () => { 21 | const { req, res } = createMocks({ 22 | method: 'POST', 23 | query: { 24 | compositionname: 'demo', 25 | }, 26 | }); 27 | 28 | await handleSave(req, res); 29 | 30 | expect(res._getStatusCode()).toBe(500); 31 | expect(res._getData()).toEqual('Composition Save: Missing Body'); 32 | }); 33 | it('returns status code 500 if there is no composition name passed', async () => { 34 | const { req, res } = createMocks({ 35 | method: 'POST', 36 | body: { name: 'name' }, 37 | }); 38 | 39 | await handleSave(req, res); 40 | 41 | expect(res._getStatusCode()).toBe(500); 42 | expect(res._getData()).toEqual( 43 | 'Composition Save: Missing Composition Name' 44 | ); 45 | }); 46 | it('if there is valid composition name and body, get the user from the session and call a DB operation', async () => { 47 | const compositionname = 'demo'; 48 | const { req, res } = createMocks({ 49 | method: 'POST', 50 | query: { 51 | compositionname: compositionname, 52 | }, 53 | body: { name: 'name' }, 54 | }); 55 | 56 | await handleSave(req, res); 57 | 58 | expect(getSession).toHaveBeenCalled(); 59 | expect(db.task).toHaveBeenCalled(); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /__tests__/flowname.spec.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Component, MouseEvent } from 'react'; 3 | import { mount, shallow } from 'enzyme'; 4 | import FlowName from '../components/FlowName'; 5 | import { Button, FormControl, FormLabel } from 'react-bootstrap'; 6 | 7 | describe('Pages', () => { 8 | describe('FlowName', () => { 9 | it('should render without with a FormLabel', function () { 10 | const wrap = shallow(); 11 | expect(wrap.find('div').find('FormLabel').text()).toBe( 12 | 'Composition Name' 13 | ); 14 | }); 15 | it('Name of the composition should be shown in an input', function () { 16 | const mockSave = jest.fn(() => Promise.resolve(true)); 17 | const wrap = shallow( 18 | 19 | ); 20 | expect(wrap.find('FormControl').props().value).toBe('DemoComposition'); 21 | }); 22 | it('Changing value will trigger props.onChange function call', function () { 23 | const onChangeMock = jest.fn(); 24 | const wrap = shallow(); 25 | const event = { 26 | preventDefault() {}, 27 | target: { value: 'DemoComposition' }, 28 | }; 29 | wrap.find('FormControl').simulate('change', event); 30 | expect(onChangeMock).toBeCalledWith('DemoComposition'); 31 | }); 32 | it('prop onSave should be called when click on save button', function () { 33 | const mockSave = jest.fn(() => Promise.resolve(true)); 34 | const wrap = shallow(); 35 | wrap.find('Button').simulate('click', { shiftKey: false }); 36 | // wrap.find('Button').props().onClick({ key: 'Enter' }); 37 | expect(mockSave).toHaveBeenCalled(); 38 | expect(mockSave.mock.calls.length).toBe(1); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /components/Execution/CloudProvidersDropdown.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { DropdownButton, Dropdown, Button } from 'react-bootstrap'; 3 | import { useDispatch } from 'react-redux'; 4 | import { setComposition } from '../../store/reducers/executionReducer'; 5 | 6 | const CloudProvidersDropdown = (props): JSX.Element => { 7 | const dispatch = useDispatch(); 8 | const dispatchSetComposition = (payload) => dispatch(setComposition(payload)); 9 | const [provider, setProvider] = useState('Cloud Providers'); // To know which cloud privider is selected 10 | 11 | const handleClick = async () => { 12 | try { 13 | const compositionResponse = await fetch( 14 | `http://localhost:3000/api/ibm/convert/${props.compositionName}` 15 | ); 16 | const compositionJSON = await compositionResponse.json(); 17 | dispatchSetComposition(JSON.stringify(compositionJSON)); 18 | } catch (error) { 19 | if (error) console.error('Error fetching'); 20 | } 21 | }; 22 | 23 | return ( 24 |
25 | {/* {composition} */} 26 | setProvider(e)} 30 | > 31 | IBM Cloud 32 | Google Cloud 33 | AWS 34 | Azure 35 | 36 |      37 | 40 | 41 | 47 |
48 | ); 49 | }; 50 | 51 | export default CloudProvidersDropdown; 52 | -------------------------------------------------------------------------------- /components/Execution/CompositionResult.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import JSONInput from 'react-json-editor-ajrm/index'; 3 | import locale from 'react-json-editor-ajrm/locale/en'; 4 | import { useSelector } from 'react-redux'; 5 | import { selectCompositionOutput } from '../../store/reducers/executionReducer'; 6 | 7 | const CompositionResult = (): JSX.Element => { 8 | const compositionOutput = useSelector(selectCompositionOutput); 9 | 10 | return ( 11 |
12 |
Output
13 | 24 |
25 | ); 26 | }; 27 | 28 | export default CompositionResult; 29 | -------------------------------------------------------------------------------- /components/Execution/Execution.tsx: -------------------------------------------------------------------------------- 1 | import CloudProvidersDropdown from './CloudProvidersDropdown'; 2 | import PlatformSpecific from './PlatformSpecific'; 3 | import UserInput from './UserInput'; 4 | import CompositionResult from './CompositionResult'; 5 | import LoginDeploy from './LoginDeploy'; 6 | 7 | const Execution = (props): JSX.Element => ( 8 | <> 9 |
10 |
Execution
11 | 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | 19 | export default Execution; 20 | -------------------------------------------------------------------------------- /components/Execution/LoginDeploy.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import JSONInput from 'react-json-editor-ajrm/index'; 3 | import locale from 'react-json-editor-ajrm/locale/en'; 4 | import { Button } from 'react-bootstrap'; 5 | import { useDispatch, useSelector } from 'react-redux'; 6 | import { Markup } from 'interweave'; 7 | import { 8 | selectUserInput, 9 | setUserInput, 10 | setCompositionOutput, 11 | } from '../../store/reducers/executionReducer'; 12 | 13 | const LoginDeploy = (props): JSX.Element => { 14 | const dispatch = useDispatch(); 15 | const userInput = useSelector(selectUserInput); 16 | const [outputText, setOutputText] = useState(''); 17 | let inputFromForm: string; 18 | 19 | const dispatchSetUserInput = (payload) => dispatch(setUserInput(payload)); 20 | const dispatchSetCompositionOutput = (payload) => dispatch(setCompositionOutput(payload)); 21 | 22 | const handleInputChange = (formInput) => { 23 | inputFromForm = formInput.json; 24 | }; 25 | 26 | const handleClick = async () => { 27 | setOutputText('loading...'); 28 | try { 29 | const resLogin = await fetch('http://localhost:3000/api/ibm/login', { 30 | method: 'post', 31 | headers: { 'Content-Type': 'application/json' }, 32 | }); 33 | const loginOutputText = await resLogin.text(); 34 | const resDeploy = await fetch( 35 | `http://localhost:3000/api/ibm/deploy/${props.compositionName}`, 36 | { 37 | method: 'post', 38 | headers: { 'Content-Type': 'application/json' }, 39 | }, 40 | ); 41 | const deployOutputText = await resDeploy.text(); 42 | let finalOutput = `${loginOutputText}\n${deployOutputText}`; 43 | finalOutput = finalOutput.replace(/\n/g, '
'); 44 | setOutputText(finalOutput); 45 | } catch (error) { 46 | // if (error) throw new Error('Error from UserInput', error); 47 | } 48 | }; 49 | 50 | return ( 51 | <> 52 |
53 | 56 |
57 |
58 | 59 |
60 | 61 | ); 62 | }; 63 | 64 | export default LoginDeploy; 65 | -------------------------------------------------------------------------------- /components/Execution/PlatformSpecific.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import JSONInput from 'react-json-editor-ajrm/index'; 3 | import locale from 'react-json-editor-ajrm/locale/en'; 4 | import { useSelector } from 'react-redux'; 5 | import { selectComposition } from '../../store/reducers/executionReducer'; 6 | 7 | const PlatformSpecific = (): JSX.Element => { 8 | const composition = useSelector(selectComposition); 9 | 10 | return ( 11 |
12 |
Platform Specific
13 | 26 |
27 | ); 28 | }; 29 | 30 | export default PlatformSpecific; 31 | -------------------------------------------------------------------------------- /components/Execution/UserInput.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import JSONInput from 'react-json-editor-ajrm/index'; 3 | import locale from 'react-json-editor-ajrm/locale/en'; 4 | import { Button } from 'react-bootstrap'; 5 | import { useDispatch, useSelector } from 'react-redux'; 6 | import { 7 | selectUserInput, 8 | setUserInput, 9 | setCompositionOutput, 10 | } from '../../store/reducers/executionReducer'; 11 | 12 | const UserInput = (props): JSX.Element => { 13 | const dispatch = useDispatch(); 14 | const userInput = useSelector(selectUserInput); 15 | let inputFromForm: string; 16 | 17 | const dispatchSetUserInput = (payload) => dispatch(setUserInput(payload)); 18 | const dispatchSetCompositionOutput = (payload) => dispatch(setCompositionOutput(payload)); 19 | 20 | const handleInputChange = (formInput) => { 21 | inputFromForm = formInput.json; 22 | }; 23 | 24 | const handleClick = async () => { 25 | const failureMsgObj = { 26 | result: 'Failure', 27 | }; 28 | dispatchSetUserInput(inputFromForm); 29 | try { 30 | const res = await fetch( 31 | `http://localhost:3000/api/ibm/invoke/${props.compositionName}`, 32 | { 33 | method: 'post', 34 | headers: { 'Content-Type': 'application/json' }, 35 | body: inputFromForm, 36 | }, 37 | ); 38 | const outputJSON = await res.json(); 39 | dispatchSetCompositionOutput(JSON.stringify(outputJSON)); 40 | } catch (error) { 41 | dispatchSetCompositionOutput(JSON.stringify(failureMsgObj)); 42 | // if (error) throw new Error('Error from UserInput', error); 43 | } 44 | }; 45 | 46 | return ( 47 | <> 48 |
Input
49 |
50 | 61 | 62 | 69 | 70 | 84 |
85 | 86 | ); 87 | }; 88 | 89 | export default UserInput; 90 | -------------------------------------------------------------------------------- /components/FlowChart-dynamic.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useReducer } from 'react'; 2 | import ReactFlow, { Background } from 'react-flow-renderer'; 3 | import { nanoid } from 'nanoid'; 4 | import { Button, FormControl, FormLabel } from 'react-bootstrap'; 5 | 6 | import { useDispatch, useSelector } from 'react-redux'; 7 | 8 | import { 9 | setCompositionName, 10 | selectCompositionName, 11 | } from '../store/reducers/executionReducer'; 12 | 13 | import { 14 | selectSequence, 15 | selectCurrentSequence, 16 | } from '../store/reducers/sequenceReducer'; 17 | import { 18 | selectClickedFunc, 19 | setCurrentFunc, 20 | } from '../store/reducers/functionsReducer'; 21 | import { 22 | setFlowRendererNodeId, 23 | selectFlowRendererNodeId, 24 | //setNodes, 25 | //updateNodeName, 26 | //selectNodes, 27 | } from '../store/reducers/canvasReducer'; 28 | 29 | import FlowName from './FlowName'; 30 | 31 | const initElements = [ 32 | { 33 | id: 'init-start', 34 | data: { label: 'Start' }, 35 | position: { x: 190, y: 5 }, 36 | style: { 37 | background: '#333', 38 | color: '#fff', 39 | border: '1px solid #bbb', 40 | width: 70, 41 | padding: 5, 42 | }, 43 | }, 44 | { 45 | id: 'init-0', 46 | data: { label: 'Choose a Flow first' }, 47 | position: { x: 75, y: 75 }, 48 | style: { 49 | fontWeight: 700, 50 | fontSize: 20, 51 | background: '#eee', 52 | color: '#333', 53 | border: '1px solid #bebebe', 54 | width: 300, 55 | }, 56 | }, 57 | 58 | { 59 | id: 'init-end', 60 | data: { label: 'End' }, 61 | position: { x: 190, y: 180 }, 62 | style: { 63 | background: '#333', 64 | color: '#fff', 65 | border: '1px solid #bbb', 66 | width: 70, 67 | padding: 5, 68 | }, 69 | }, 70 | { id: 'init-e1-3', source: 'init-start', target: 'init-0', animated: true }, 71 | { id: 'init-e1-4', source: 'init-0', target: 'init-end', animated: true }, 72 | ]; 73 | 74 | const elements = [ 75 | { 76 | id: 'sequence-start', 77 | data: { label: 'Start' }, 78 | position: { x: 190, y: 5 }, 79 | style: { 80 | background: '#333', 81 | color: '#fff', 82 | border: '1px solid #bbb', 83 | width: 70, 84 | padding: 5, 85 | }, 86 | }, 87 | { 88 | id: 'sequence-0', 89 | data: { label: 'Node 1' }, 90 | position: { x: 150, y: 75 }, 91 | style: { fontWeight: 400, fontSize: 15, background: '#eee', color: '#333' }, 92 | }, 93 | { 94 | id: 'sequence-1', 95 | data: { label: 'Node 2' }, 96 | position: { x: 150, y: 150 }, 97 | style: { fontWeight: 400, fontSize: 15, background: '#eee', color: '#333' }, 98 | }, 99 | { 100 | id: 'sequence-end', 101 | data: { label: 'End' }, 102 | position: { x: 190, y: 225 }, 103 | style: { 104 | background: '#333', 105 | color: '#fff', 106 | border: '1px solid #bbb', 107 | width: 70, 108 | padding: 5, 109 | }, 110 | }, 111 | { id: 'e1-2', source: 'sequence-0', target: 'sequence-1', animated: true }, 112 | { 113 | id: 'e1-3', 114 | source: 'sequence-start', 115 | target: 'sequence-0', 116 | animated: true, 117 | }, 118 | { id: 'e1-4', source: 'sequence-1', target: 'sequence-end', animated: true }, 119 | ]; 120 | 121 | const elements_ifelse = [ 122 | { 123 | id: 'ifelse-start', 124 | data: { label: 'Start' }, 125 | position: { x: 190, y: 5 }, 126 | style: { 127 | background: '#333', 128 | color: '#fff', 129 | border: '1px solid #bbb', 130 | width: 70, 131 | padding: 5, 132 | }, 133 | }, 134 | { 135 | id: 'ifelse-0', 136 | data: { label: 'Node 1' }, 137 | position: { x: 150, y: 75 }, 138 | style: { fontWeight: 400, fontSize: 15, background: '#eee', color: '#333' }, 139 | }, 140 | { 141 | id: 'ifelse-1', 142 | data: { label: 'Node 2' }, 143 | position: { x: 50, y: 150 }, 144 | style: { fontWeight: 400, fontSize: 15, background: '#eee', color: '#333' }, 145 | }, 146 | { 147 | id: 'ifelse-2', 148 | data: { label: 'Node 3' }, 149 | position: { x: 250, y: 150 }, 150 | style: { fontWeight: 400, fontSize: 15, background: '#eee', color: '#333' }, 151 | }, 152 | { 153 | id: 'ifelse-end', 154 | data: { label: 'End' }, 155 | position: { x: 190, y: 225 }, 156 | style: { 157 | background: '#333', 158 | color: '#fff', 159 | border: '1px solid #bbb', 160 | width: 70, 161 | padding: 5, 162 | }, 163 | }, 164 | { id: 'e2-2', source: 'ifelse-start', target: 'ifelse-0', animated: true }, 165 | { 166 | id: 'e2-3', 167 | source: 'ifelse-0', 168 | target: 'ifelse-1', 169 | animated: false, 170 | type: 'smoothstep', 171 | arrowHeadType: 'arrowclosed', 172 | label: 'true', 173 | }, 174 | { 175 | id: 'e2-4', 176 | source: 'ifelse-0', 177 | target: 'ifelse-2', 178 | animated: false, 179 | type: 'smoothstep', 180 | arrowHeadType: 'arrowclosed', 181 | style: { stroke: '#f6ab6c' }, 182 | label: 'false', 183 | }, 184 | { 185 | id: 'e2-5', 186 | source: 'ifelse-1', 187 | target: 'ifelse-end', 188 | animated: false, 189 | type: 'smoothstep', 190 | }, 191 | { 192 | id: 'e2-6', 193 | source: 'ifelse-2', 194 | target: 'ifelse-end', 195 | animated: false, 196 | type: 'smoothstep', 197 | style: { stroke: '#f6ab6c' }, 198 | }, 199 | ]; 200 | 201 | export const combineResult = (name, flowType, nodes) => { 202 | const tempFunc = nodes 203 | .filter((node) => 204 | node.data !== undefined && 205 | node.data.label !== 'Start' && 206 | node.data.label !== 'End' 207 | ? node 208 | : '' 209 | ) 210 | .map((e) => { 211 | return e.data.funcID; 212 | }); 213 | return { name, type: flowType, func: tempFunc }; 214 | }; 215 | 216 | const BasicFlow = (props) => { 217 | const reduxDispatch = useDispatch(); 218 | const compositionName = useSelector(selectCompositionName); 219 | const sequences = useSelector(selectSequence); 220 | const selectedCurrentSequence = useSelector(selectCurrentSequence); 221 | const selectedFunctions = useSelector(selectClickedFunc); 222 | const selectedFlowRendererNodeId = useSelector(selectFlowRendererNodeId); 223 | //const nodes = useSelector(selectNodes); 224 | 225 | let updateSequence = () => { 226 | let nodeValue; 227 | if (selectedCurrentSequence.toString() == 'sequence') { 228 | //return elements; 229 | nodeValue = elements; 230 | // reduxDispatch(setNodes(elements)); 231 | } else if (selectedCurrentSequence.toString() == 'ifelse') { 232 | // reduxDispatch(setNodes(elements_ifelse)); 233 | // return elements_ifelse; 234 | nodeValue = elements_ifelse; 235 | } else { 236 | // reduxDispatch(setNodes(initElements)); //initElements;} 237 | nodeValue = initElements; 238 | } 239 | return nodeValue; 240 | }; 241 | let nodes = updateSequence(); 242 | let updateFunction = () => { 243 | let newState = nodes.map((node) => { 244 | if (node.id == selectedFlowRendererNodeId) { 245 | node.data = { 246 | label: selectedFunctions.name, 247 | funcID: selectedFunctions.id, 248 | }; 249 | node.style = { background: '#8DA9C4' }; 250 | } 251 | return node; 252 | }); 253 | 254 | return newState; 255 | }; 256 | 257 | nodes = updateFunction(); 258 | useEffect(() => { 259 | updateFunction(); 260 | updateSequence(); 261 | }); 262 | 263 | const resultFunc = combineResult( 264 | compositionName, 265 | selectedCurrentSequence, 266 | nodes 267 | ); 268 | const onElementClick = (event, element) => { 269 | reduxDispatch(setFlowRendererNodeId(element.id)); 270 | reduxDispatch(setCurrentFunc({ id: '', name: '' })); 271 | }; 272 | 273 | function changeCompositionName(name) { 274 | reduxDispatch(setCompositionName(name)); 275 | } 276 | 277 | return ( 278 |
279 | 284 | 285 | 286 | { 288 | props.onSave(resultFunc); 289 | }} 290 | onChange={(name) => { 291 | changeCompositionName(name); 292 | }} 293 | compositionName={compositionName} 294 | /> 295 |
296 | ); 297 | }; 298 | 299 | export default BasicFlow; 300 | -------------------------------------------------------------------------------- /components/FlowChart.tsx: -------------------------------------------------------------------------------- 1 | 2 | import React, {useState,useEffect, useReducer, useContext, createContext} from 'react'; 3 | import ReactFlow, { useStoreState, Background } from 'react-flow-renderer'; 4 | import { nanoid } from 'nanoid'; 5 | 6 | 7 | 8 | 9 | const initElements=[ 10 | { id: 'init-start', data: { label: 'Start' }, position: { x: 190, y: 5 }, style: { background: '#333', color: '#fff', border: '1px solid #bbb', width: 70 , padding:5} }, 11 | { id: 'init-0', data: { label: 'Choose a Flow first' }, position: { x: 75, y: 75 }, style: { fontWeight: 700,fontSize: 20, background: '#eee', color: '#333', border: '1px solid #bebebe', width: 300 },}, 12 | 13 | { id: 'init-end', data: { label: 'End' }, position: { x: 190, y: 180 }, style: { background: '#333', color: '#fff', border: '1px solid #bbb', width: 70 , padding:5} }, 14 | { id: 'init-e1-3', source: 'init-start', target: 'init-0', animated: true }, 15 | { id: 'init-e1-4', source: 'init-0', target: 'init-end', animated: true }, 16 | ]; 17 | 18 | 19 | const elements = [ 20 | { id: 'sequence-start', data: { label: 'Start' }, position: { x: 190, y: 5 }, style: { background: '#333', color: '#fff', border: '1px solid #bbb', width: 70 , padding:5} }, 21 | { id: 'sequence-0', data: { label: 'Node 1' }, position: { x: 150, y: 75 }, style: { fontWeight: 400,fontSize: 15, background: '#eee', color: '#333' } }, 22 | { id: 'sequence-1', data: { label: 'Node 2' }, position: { x: 150, y: 150 }, style: { fontWeight: 400,fontSize: 15, background: '#eee', color: '#333' } }, 23 | { id: 'sequence-end', data: { label: 'End' }, position: { x: 190, y: 225 }, style: { background: '#333', color: '#fff', border: '1px solid #bbb', width: 70 , padding:5} }, 24 | { id: 'e1-2', source: 'sequence-0', target: 'sequence-1', animated: true }, 25 | { id: 'e1-3', source: 'sequence-start', target: 'sequence-0', animated: true }, 26 | { id: 'e1-4', source: 'sequence-1', target: 'sequence-end', animated: true }, 27 | ]; 28 | 29 | const elements_ifelse=[ 30 | { id: 'ifelse-start', data: { label: 'Start' }, position: { x: 190, y: 5 }, style: { background: '#333', color: '#fff', border: '1px solid #bbb', width: 70 , padding:5} }, 31 | { id: 'ifelse-0', data: { label: 'Node 1' }, position: { x: 150, y: 75 }, style: { fontWeight: 400,fontSize: 15, background: '#eee', color: '#333' } }, 32 | { id: 'ifelse-1', data: { label: 'Node 2' }, position: { x: 50, y: 150 }, style: { fontWeight: 400,fontSize: 15, background: '#eee', color: '#333' } }, 33 | { id: 'ifelse-2', data: { label: 'Node 3' }, position: { x: 250, y: 150 }, style: { fontWeight: 400,fontSize: 15, background: '#eee', color: '#333' } }, 34 | { id: 'ifelse-end', data: { label: 'End' }, position: { x: 190, y: 225 }, style: { background: '#333', color: '#fff', border: '1px solid #bbb', width: 70 , padding:5} }, 35 | { id: 'e2-2', source: 'ifelse-start', target: 'ifelse-0', animated: true, }, 36 | { id: 'e2-3', source: 'ifelse-0', target: 'ifelse-1', animated: false ,type: 'smoothstep', arrowHeadType: 'arrowclosed'}, 37 | { id: 'e2-4', source: 'ifelse-0', target: 'ifelse-2', animated: false ,type: 'smoothstep', arrowHeadType: 'arrowclosed', style: { stroke: '#f6ab6c' },}, 38 | { id: 'e2-5', source: 'ifelse-1', target: 'ifelse-end', animated: false ,type: 'smoothstep'}, 39 | { id: 'e2-6', source: 'ifelse-2', target: 'ifelse-end', animated: false ,type: 'smoothstep', style: { stroke: '#f6ab6c' }} 40 | ]; 41 | 42 | 43 | export const ACTIONS = { 44 | ADD: "ADD", 45 | REMOVE: "REMOVE", 46 | //MODIFY_LABEL: "MODIFY_LABEL", 47 | RESET: "RESET", 48 | UPDATE_POSITION: "UPDATE_POSITION", 49 | SEQUENCE: "SEQUENCE", 50 | FUNCTIONS: "FUNCTIONS", 51 | 52 | }; 53 | 54 | const reducer = ( state, action)=>{ 55 | console.log('reducer::', state, action); 56 | switch (action.type) { 57 | case ACTIONS.SEQUENCE: { 58 | console.log('ACTIONS.SEQUENCE', action) 59 | if(action.payload=='sequence'){ return elements } 60 | else if(action.payload=='ifelse') { return elements_ifelse } 61 | else return initElements; 62 | } 63 | case ACTIONS.FUNCTIONS: { 64 | let newState = state.map(node=>{ 65 | 66 | 67 | if(node.id==action.payload.target){ 68 | node.data = {label:action.payload.functionNames}; 69 | node.style={background:"#8DA9C4"} 70 | } 71 | return node; 72 | }); 73 | 74 | return newState; 75 | } 76 | 77 | case ACTIONS.ADD: { 78 | return [ 79 | ...state, 80 | { 81 | id: nanoid(), 82 | data: {label: action.label}, 83 | position:{x: 100, y: 100 } 84 | }, 85 | ]; 86 | } 87 | case ACTIONS.REMOVE: { 88 | return state.filter((node) => node.id !== action.id); 89 | } 90 | default: 91 | return state; 92 | }; 93 | }; 94 | 95 | 96 | const BasicFlow = (props) =>{ 97 | 98 | const [nodes, dispatch] = useReducer(reducer, initElements); 99 | const [type, setType] = useState('sequence'); 100 | const [functions, setFunctions]= useState(); 101 | const [target, setTarget]=useState(''); 102 | 103 | useEffect(() => { 104 | //update in sequence 105 | setType(()=>{ 106 | if(type!=props.type){ 107 | dispatch({ type: ACTIONS.SEQUENCE, payload:props.type}); 108 | return props.type; 109 | } 110 | else if(type==props.type) return type; 111 | }); 112 | 113 | //update functions name 114 | setFunctions(()=>{ 115 | if(props.functionNames!=functions && target !== undefined){ 116 | dispatch({ 117 | type: ACTIONS.FUNCTIONS, 118 | payload:{target: target, functionNames: props.functionNames }}); 119 | return props.functionNames; 120 | } 121 | else return functions; 122 | }); 123 | 124 | }) ; 125 | 126 | const onElementClick = (event, element) => setTarget(element.id); 127 | 128 | 129 | return ( 130 | 134 | 135 | 136 | ) 137 | }; 138 | 139 | export default BasicFlow; 140 | -------------------------------------------------------------------------------- /components/FlowName.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, FormControl, FormLabel } from 'react-bootstrap'; 3 | 4 | const FlowName = (props) => { 5 | return ( 6 |
7 | Composition Name 8 | { 13 | props.onChange(e.target.value); 14 | }} 15 | /> 16 | 24 |
25 | ); 26 | }; 27 | 28 | export default FlowName; 29 | -------------------------------------------------------------------------------- /components/FlowStructureButton.tsx: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid'; 2 | import React, { useState } from 'react'; 3 | import { Button } from 'react-bootstrap'; 4 | //import flowStructure from '../data/flowStructures.json'; 5 | import { useSelector, useDispatch } from 'react-redux'; 6 | 7 | import { 8 | selectSequence, 9 | changeCurrent, 10 | selectCurrentSequence, 11 | } from '../store/reducers/sequenceReducer'; 12 | 13 | const FlowButtons = (): JSX.Element => { 14 | const sequence = useSelector(selectSequence); 15 | const selectedCurrentSequence = useSelector(selectCurrentSequence); 16 | const dispatch = useDispatch(); 17 | 18 | return ( 19 | <> 20 |

21 | Choose a flow 22 |

23 | {sequence['list'].map((button) => ( 24 | 36 | ))} 37 | 38 | ); 39 | }; 40 | 41 | export default FlowButtons; 42 | -------------------------------------------------------------------------------- /components/FuncEditor.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import { Button, Form, Modal } from 'react-bootstrap'; 3 | import { useDispatch, useSelector } from 'react-redux'; 4 | import Editor from '@monaco-editor/react'; 5 | import { nanoid } from 'nanoid'; 6 | 7 | import { toggleFuncEditor, selectShow } from '../store/reducers/editorReducer'; 8 | import { 9 | addFunc, 10 | selectFuncToEdit, 11 | setCurrentFunc, 12 | } from '../store/reducers/functionsReducer'; 13 | 14 | const FuncEditor = (): JSX.Element => { 15 | const [isEditorReady, setIsEditorReady] = useState(false); 16 | const dispatch = useDispatch(); 17 | const editorView = useSelector(selectShow); 18 | const funcToEdit = useSelector(selectFuncToEdit); 19 | // This is to get value of text in editor 20 | const valueGetter = useRef(); 21 | 22 | // console.log('Description: ', funcToEdit.description); 23 | const handleEditorDidMount = (_valueGetter) => { 24 | setIsEditorReady(true); 25 | valueGetter.current = _valueGetter; 26 | }; 27 | 28 | function dispatchToggleFuncEditor() { 29 | dispatch(toggleFuncEditor()); 30 | } 31 | 32 | // Add function method, runs when "Add Function" button pressed 33 | const addFuncToReduxAndBackend = () => { 34 | let id; 35 | if (funcToEdit.id) { 36 | id = funcToEdit.id; 37 | } 38 | 39 | const name = (document.getElementById('name') as HTMLTextAreaElement).value; 40 | // get description of functi on from form 41 | const description = (document.getElementById( 42 | 'description' 43 | ) as HTMLTextAreaElement).value; 44 | // Create object to hold new function with all values set 45 | const newFuncObj = { 46 | name, 47 | id, 48 | description, 49 | // This gets the value from the Editor 50 | definition: valueGetter.current(), 51 | }; 52 | dispatch(addFunc(newFuncObj)); 53 | if (funcToEdit.id) dispatch(setCurrentFunc(newFuncObj.name)); 54 | 55 | fetch('/api/functions/upsert-function', { 56 | method: 'post', 57 | headers: { 58 | 'Content-Type': 'application/json', 59 | }, 60 | body: JSON.stringify(newFuncObj), 61 | }).then(() => { 62 | setTimeout(() => { 63 | dispatchToggleFuncEditor(); 64 | }, 500); 65 | }); 66 | }; 67 | 68 | return ( 69 | 70 | 71 |
Function Editor
72 |
73 | 74 | 82 |
83 | Function Name: 84 | 89 | Function Description: 90 | 95 | 96 | 103 | 106 |
107 |
108 |
109 | ); 110 | }; 111 | 112 | export default FuncEditor; 113 | -------------------------------------------------------------------------------- /components/FunctionInventory.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { 4 | Card, 5 | Button, 6 | ListGroup, 7 | ListGroupItem, 8 | Tooltip, 9 | OverlayTrigger, 10 | } from 'react-bootstrap'; 11 | import { useSession } from 'next-auth/client'; 12 | 13 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 14 | import { faEdit } from '@fortawesome/free-solid-svg-icons'; 15 | 16 | import { toggleFuncEditor } from '../store/reducers/editorReducer'; 17 | import { 18 | setFuncs, 19 | selectFuncs, 20 | setCurrentFunc, 21 | setFuncToEdit, 22 | } from '../store/reducers/functionsReducer'; 23 | 24 | const FunctionInventory = (): JSX.Element => { 25 | const [session, loading] = useSession(); 26 | const dispatch = useDispatch(); 27 | const currentFuncs = useSelector(selectFuncs); 28 | const funcs = []; 29 | 30 | function dispatchToggleFuncEditor() { 31 | dispatch(toggleFuncEditor()); 32 | } 33 | 34 | const getFuncs = () => { 35 | fetch('/api/functions/read-functions', { 36 | method: 'GET', 37 | headers: { 38 | 'Content-Type': 'application/json', 39 | }, 40 | }) 41 | .then((response) => response.json()) 42 | .then((data) => { 43 | dispatch(setFuncs(data)); 44 | }); 45 | }; 46 | useEffect(() => { 47 | getFuncs(); 48 | }, []); 49 | 50 | for (const func in currentFuncs) { 51 | funcs.push( 52 | { 61 | dispatch( 62 | setCurrentFunc({ 63 | id: currentFuncs[func].id, 64 | name: currentFuncs[func].name, 65 | }) 66 | ); 67 | }} 68 | > 69 | 73 | {currentFuncs[func].description} 74 | 75 | } 76 | > 77 | {currentFuncs[func].name} 78 | 79 | { 81 | dispatch(setFuncToEdit(currentFuncs[func])); 82 | dispatchToggleFuncEditor(); 83 | }} 84 | icon={faEdit} 85 | className="icon float-right" 86 | /> 87 | 88 | ); 89 | } 90 | 91 | return ( 92 |
93 | 94 | 95 | Cloud Function Inventory 96 | {funcs} 97 | 98 | 115 | 116 | 117 |
118 | ); 119 | }; 120 | 121 | export default FunctionInventory; 122 | -------------------------------------------------------------------------------- /components/FunctionStructureButtons.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Button, Container, Row, Col, Card, 4 | } from 'react-bootstrap'; 5 | import functionsStructure from '../data/users/functions.json'; 6 | 7 | const FunctionButtons = (props): JSX.Element => { 8 | const [buttons, setButtons] = useState(functionsStructure); 9 | 10 | return ( 11 | <> 12 |

13 | Add functions 14 |

15 | 16 | {Object.keys(buttons).map((button) => ( 17 | 28 | ))} 29 | 30 | ); 31 | }; 32 | 33 | export default FunctionButtons; 34 | // active={props.functions.includes(buttons[button].name) ? true :false} 35 | -------------------------------------------------------------------------------- /components/Nav.tsx: -------------------------------------------------------------------------------- 1 | import { signin, signout, useSession } from 'next-auth/client'; 2 | import styles from '../styles/nav.module.css'; 3 | 4 | /** 5 | * The approach used in this component shows how to built a sign in and sign out 6 | * component that works on pages which support both client and server side 7 | * rendering, and avoids any flash incorrect content on initial page load. 8 | **/ 9 | const Nav = () => { 10 | const [session, loading] = useSession(); 11 | 12 | return ( 13 | 58 | ); 59 | }; 60 | 61 | export default Nav; 62 | -------------------------------------------------------------------------------- /components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const NavBar = (): JSX.Element => ( 4 |
8 |

FaaSCompose

9 |
10 | ); 11 | 12 | export default NavBar; 13 | -------------------------------------------------------------------------------- /data/db.ts: -------------------------------------------------------------------------------- 1 | // Proper way to initialize and share the Database object 2 | 3 | // Loading and initializing the library: 4 | const pgp = require('pg-promise')({ 5 | // Initialization Options 6 | }); 7 | 8 | // Preparing the connection details: 9 | const cn = process.env.NEXTAUTH_DATABASE_URL; 10 | 11 | // Creating a new database instance from the connection details: 12 | const db = pgp(cn); 13 | 14 | // Exporting the database object for shared use: 15 | export default db; 16 | -------------------------------------------------------------------------------- /data/flowStructures.json: -------------------------------------------------------------------------------- 1 | { 2 | "sequence": { 3 | "id": "8f7b25ed-b7f9-4139-8640-6ba32600ab5f", 4 | "display": "Sequence" 5 | }, 6 | "ifelse": { 7 | "id": "3556d8dd-5da1-48b0-a851-1bad67a7f252", 8 | "display": "If Else" 9 | } 10 | } -------------------------------------------------------------------------------- /data/users/compositions/demo-agnostic.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "demo", 3 | "type": "sequence", 4 | "func": ["success authenticate", "Hello World"] 5 | } 6 | -------------------------------------------------------------------------------- /data/users/compositions/demo.js: -------------------------------------------------------------------------------- 1 | const composer = require('openwhisk-composer'); 2 | -------------------------------------------------------------------------------- /data/users/compositions/demo.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/FaaSCompose/090d084823f0b64161bc4d23c1f978a53ebd8b3c/data/users/compositions/demo.json -------------------------------------------------------------------------------- /data/users/functions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": { 3 | "name": "authenticate", 4 | "id": "1", 5 | "description": "function to authenticate users", 6 | "definition": "function ({ password }) { return { value: password === 'abc123' }" 7 | }, 8 | "2": { 9 | "id": "2", 10 | "name": "success authenticate", 11 | "description": "function to authenticate users", 12 | "definition": "function () { return { message: 'success' }" 13 | }, 14 | "3": { 15 | "id": "3", 16 | "name": "failure authenticate", 17 | "description": "function to authenticate users", 18 | "definition": "function () { return { message: 'failure' }" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /decl.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-json-editor-ajrm/index'; 2 | declare module 'react-json-editor-ajrm/locale/en'; 3 | -------------------------------------------------------------------------------- /enzyme.js: -------------------------------------------------------------------------------- 1 | const Adapter = require('enzyme-adapter-react-16'); 2 | 3 | require('enzyme').configure({ adapter: new Adapter() }); 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['ts', 'tsx', 'js'], 3 | transform: { 4 | '^.+\\.tsx?$': 'ts-jest', 5 | }, 6 | testMatch: ['**/*.(test|spec).(ts|tsx|js)'], 7 | globals: { 8 | 'ts-jest': { 9 | babelConfig: '.babelrc', 10 | tsConfig: 'jest.tsconfig.json', 11 | }, 12 | }, 13 | coveragePathIgnorePatterns: ['/node_modules/', 'enzyme.js'], 14 | setupTestFrameworkScriptFile: '/enzyme.js', 15 | coverageReporters: ['json', 'lcov', 'text', 'text-summary'], 16 | moduleNameMapper: { 17 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 18 | '/__mocks__/mocks.js', 19 | '\\.(css|less|scss)$': '/__mocks__/mocks.js', 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /jest.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "esnext", 5 | "jsx": "react", 6 | "sourceMap": false, 7 | "experimentalDecorators": true, 8 | "noImplicitUseStrict": true, 9 | "removeComments": true, 10 | "moduleResolution": "node", 11 | "lib": ["es2017", "dom"], 12 | "typeRoots": ["node_modules/@types"], 13 | "esModuleInterop": true 14 | }, 15 | "exclude": ["node_modules", "out", ".next"] 16 | } 17 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | // / 2 | // / 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "faascompose", 3 | "version": "0.1.0", 4 | "description": "Graphical User Interface for Composing FaaS Workflows", 5 | "private": true, 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "type-check": "tsc --pretty --noEmit", 11 | "format": "prettier --write \"**/*.{js,ts,tsx}\"", 12 | "lint": "eslint . --ext ts --ext tsx --ext js", 13 | "test": "jest", 14 | "test-all": "yarn lint && yarn type-check && yarn test", 15 | "test:watch": "jest --watch", 16 | "test:coverage": "jest --coverage" 17 | }, 18 | "husky": { 19 | "hooks": { 20 | "pre-commit": "lint-staged", 21 | "pre-push": "yarn run type-check" 22 | } 23 | }, 24 | "lint-staged": { 25 | "*.@(ts|tsx)": [ 26 | "yarn lint", 27 | "yarn format" 28 | ] 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/oslabs-beta/FaaSCompose.git" 33 | }, 34 | "keywords": [ 35 | "faas", 36 | "compose", 37 | "workflow", 38 | "serverless" 39 | ], 40 | "author": "FaaSCompose", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/oslabs-beta/FaaSCompose/issues" 44 | }, 45 | "homepage": "https://github.com/oslabs-beta/FaaSCompose#readme", 46 | "dependencies": { 47 | "@fortawesome/fontawesome-svg-core": "^1.2.30", 48 | "@fortawesome/free-solid-svg-icons": "^5.14.0", 49 | "@fortawesome/react-fontawesome": "^0.1.11", 50 | "@monaco-editor/react": "^3.5.7", 51 | "@reduxjs/toolkit": "^1.4.0", 52 | "@testing-library/jest-dom": "^5.11.4", 53 | "@types/next-auth": "^3.1.10", 54 | "bootstrap": "^4.5.2", 55 | "esprima": "^4.0.1", 56 | "fs": "0.0.1-security", 57 | "interweave": "^12.5.0", 58 | "jquery": "^3.5.1", 59 | "monaco-editor": "^0.20.0", 60 | "nanoid": "^3.1.12", 61 | "next": "9.5.2", 62 | "next-auth": "^3.1.0", 63 | "node-mocks-http": "^1.9.0", 64 | "openwhisk-composer": "^0.12.0", 65 | "pg": "^8.3.3", 66 | "pg-promise": "^10.6.2", 67 | "popper.js": "^1.16.1", 68 | "prop-types": "^15.7.2", 69 | "react": "16.13.1", 70 | "react-bootstrap": "^1.3.0", 71 | "react-dom": "^16.13.1", 72 | "react-flow-renderer": "^5.1.2", 73 | "react-json-editor-ajrm": "^2.5.9", 74 | "react-redux": "^7.2.1", 75 | "redux": "^4.0.5", 76 | "redux-devtools-extension": "^2.13.8", 77 | "shelljs": "^0.8.4" 78 | }, 79 | "devDependencies": { 80 | "@testing-library/react": "^10.4.9", 81 | "@types/enzyme": "^3.10.6", 82 | "@types/enzyme-adapter-react-16": "^1.0.6", 83 | "@types/jest": "^26.0.14", 84 | "@types/next-auth": "^3.1.10", 85 | "@types/node": "^14.0.27", 86 | "@types/prop-types": "^15.7.3", 87 | "@types/react": "^16.9.46", 88 | "@types/react-bootstrap": "^0.32.22", 89 | "@types/react-redux": "^7.1.9", 90 | "@types/testing-library__react": "^10.2.0", 91 | "@typescript-eslint/eslint-plugin": "^3.9.0", 92 | "@typescript-eslint/parser": "^3.9.0", 93 | "babel-core": "^6.26.3", 94 | "babel-jest": "^26.3.0", 95 | "babel-preset-env": "^1.7.0", 96 | "babel-preset-react": "^6.24.1", 97 | "enzyme": "^3.11.0", 98 | "enzyme-adapter-react-16": "^1.15.4", 99 | "eslint": "^7.6.0", 100 | "eslint-config-prettier": "^6.11.0", 101 | "eslint-plugin-react": "^7.20.5", 102 | "husky": "^4.2.5", 103 | "jest": "^26.4.2", 104 | "jest-watch-typeahead": "^0.6.0", 105 | "lint-staged": "^10.2.11", 106 | "prettier": "^2.0.5", 107 | "ts-jest": "^26.3.0", 108 | "typescript": "^3.9.7" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import 'bootstrap/dist/css/bootstrap.min.css'; 3 | 4 | import type { AppProps /* , AppContext */ } from 'next/app'; 5 | 6 | import { Provider } from 'react-redux'; 7 | import { Provider as AuthProvider } from 'next-auth/client'; 8 | import store from '../store/store'; 9 | import NavBar from '../components/NavBar'; 10 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 11 | function MyApp({ Component, pageProps }: AppProps) { 12 | const { session } = pageProps; 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | // Only uncomment this method if you have blocking data requirements for 24 | // every single page in your application. This disables the ability to 25 | // perform automatic static optimization, causing every page in your app to 26 | // be server-side rendered. 27 | // 28 | // MyApp.getInitialProps = async (appContext: AppContext) => { 29 | // // calls page's `getInitialProps` and fills `appProps.pageProps` 30 | // const appProps = await App.getInitialProps(appContext); 31 | 32 | // return { ...appProps } 33 | // } 34 | 35 | export default MyApp; 36 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import Providers from 'next-auth/providers'; 3 | 4 | const options = { 5 | // @link https://next-auth.js.org/configuration/providers 6 | providers: [ 7 | // When configuring oAuth providers make sure you enabling requesting 8 | // permission to get the users email address (required to sign in) 9 | Providers.GitHub({ 10 | clientId: process.env.NEXTAUTH_GITHUB_ID, 11 | clientSecret: process.env.NEXTAUTH_GITHUB_SECRET, 12 | }), 13 | ], 14 | 15 | // @link https://next-auth.js.org/configuration/databases 16 | database: process.env.NEXTAUTH_DATABASE_URL, 17 | 18 | // @link https://next-auth.js.org/configuration/options#session 19 | session: { 20 | // Use JSON Web Tokens for session instead of database sessions. 21 | // This option can be used with or without a database for users/accounts. 22 | // Note: `jwt` is automatically set to `true` if no database is specified. 23 | jwt: true, 24 | // Seconds - How long until an idle session expires and is no longer valid. 25 | maxAge: 30 * 24 * 60 * 60, // 30 days 26 | // Seconds - Throttle how frequently to write to database to extend a session. 27 | // Use it to limit write operations. Set to 0 to always update the database. 28 | // Note: This option is ignored if using JSON Web Tokens 29 | // updateAge: 24 * 60 * 60, // 24 hours 30 | }, 31 | 32 | // @link https://next-auth.js.org/configuration/options#jwt 33 | jwt: { 34 | // A secret to use for key generation - you should set this explicitly 35 | // Defaults to NextAuth.js secret if not explicitly specified. 36 | // secret: 'INp8IvdIyeMcoGAgFGoA61DdBglwwSqnXJZkgz8PSnw', 37 | // Set to true to use encryption. Defaults to false (signing only). 38 | encryption: true, 39 | // You can define your own encode/decode functions for signing and encryption 40 | // if you want to override the default behaviour. 41 | // encode: async ({ secret, token, maxAge }) => {}, 42 | // decode: async ({ secret, token, maxAge }) => {}, 43 | }, 44 | 45 | // @link https://next-auth.js.org/configuration/callbacks 46 | callbacks: { 47 | /** 48 | * Intercept signIn request and return true if the user is allowed. 49 | * 50 | * @link https://next-auth.js.org/configuration/callbacks#sign-in-callback 51 | * @param {object} user User object 52 | * @param {object} account Provider account 53 | * @param {object} profile Provider profile 54 | * @return {boolean} Return `true` (or a modified JWT) to allow sign in 55 | * Return `false` to deny access 56 | */ 57 | signIn: async (user, account, profile) => { 58 | return true; 59 | }, 60 | 61 | /** 62 | * @link https://next-auth.js.org/configuration/callbacks#session-callback 63 | * @param {object} session Session object 64 | * @param {object} user User object (if using database sessions) 65 | * JSON Web Token (if not using database sessions) 66 | * @return {object} Session that will be returned to the client 67 | */ 68 | session: async (session, user) => { 69 | //session.customSessionProperty = 'bar' 70 | return Promise.resolve(session); 71 | }, 72 | 73 | /** 74 | * @link https://next-auth.js.org/configuration/callbacks#jwt-callback 75 | * @param {object} token Decrypted JSON Web Token 76 | * @param {object} user User object (only available on sign in) 77 | * @param {object} account Provider account (only available on sign in) 78 | * @param {object} profile Provider profile (only available on sign in) 79 | * @param {boolean} isNewUser True if new user (only available on sign in) 80 | * @return {object} JSON Web Token that will be saved 81 | */ 82 | jwt: async (token, user, account, profile, isNewUser) => { 83 | //const isSignIn = (user) ? true : false 84 | // Add auth_time to token on signin in 85 | //if (isSignIn) { token.auth_time = Math.floor(Date.now() / 1000) } 86 | return Promise.resolve(token); 87 | }, 88 | }, 89 | 90 | // You can define custom pages to override the built-in pages 91 | // The routes shown here are the default URLs that will be used. 92 | // @link https://next-auth.js.org/configuration/pages 93 | pages: { 94 | //signIn: '/api/auth/signin', 95 | //signOut: '/api/auth/signout', 96 | //error: '/api/auth/error', // Error code passed in query string as ?error= 97 | //verifyRequest: '/api/auth/verify-request', // (used for check email message) 98 | //newUser: null // If set, new users will be directed here on first sign in 99 | }, 100 | 101 | // Additional options 102 | // secret: 'abcdef123456789' // Recommended (but auto-generated if not specified) 103 | // debug: true, // Use this option to enable debug messages in the console 104 | events: { 105 | signIn: async (message) => { 106 | /* on successful sign in */ 107 | }, 108 | signOut: async (message) => { 109 | /* on signout */ 110 | }, 111 | createUser: async (message) => { 112 | /* user created */ 113 | }, 114 | linkAccount: async (message) => { 115 | /* account linked to a user */ 116 | }, 117 | session: async (message) => { 118 | /* session is active */ 119 | }, 120 | error: async (message) => { 121 | /* error in authentication flow */ 122 | }, 123 | }, 124 | }; 125 | 126 | const Auth = (req, res) => NextAuth(req, res, options); 127 | 128 | export default Auth; 129 | -------------------------------------------------------------------------------- /pages/api/composition/agnosticsave/[compositionname].ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | import db from '../../../../data/db'; 4 | import { getSession } from 'next-auth/client'; 5 | 6 | const compositionsDirectory = path.join( 7 | process.cwd(), 8 | 'data/users/compositions' 9 | ); 10 | 11 | export const config = { 12 | api: { 13 | bodyParser: { 14 | sizeLimit: '1mb', 15 | }, 16 | }, 17 | }; 18 | 19 | export default async (req, res) => { 20 | const { 21 | query: { compositionname }, 22 | } = req; 23 | if ( 24 | !req.body || 25 | (Object.keys(req.body).length === 0 && req.body.constructor === Object) 26 | ) { 27 | res.statusCode = 500; 28 | return res.send('Composition Save: Missing Body'); 29 | } 30 | if (!compositionname) { 31 | res.statusCode = 500; 32 | return res.send('Composition Save: Missing Composition Name'); 33 | } 34 | // const agnosticfilePath = path.join( 35 | // compositionsDirectory, 36 | // `${compositionname}-agnostic.json` 37 | // ); 38 | 39 | const session = await getSession({ req }); 40 | // let id = ; 41 | db.task((t) => { 42 | return t 43 | .one( 44 | 'SELECT id FROM users WHERE name = $1', 45 | session.user.name, 46 | (a) => a.id 47 | ) 48 | .then((userid) => { 49 | // console.log('USERID: ', userid); 50 | return t 51 | .any( 52 | 'SELECT id FROM compositions WHERE name = $1 AND userid = $2', 53 | [compositionname, userid], 54 | (a) => a.id 55 | ) 56 | .then((compositions) => { 57 | console.log('compositions', compositions); 58 | 59 | if (compositions.length === 0) { 60 | console.log('no ID'); 61 | return t.any( 62 | `INSERT INTO compositions(name, description, definition, userid) 63 | VALUES($1, $2, $3, $4)`, 64 | [compositionname, '', JSON.stringify(req.body), userid] 65 | ); 66 | } else { 67 | console.log('ID exists'); 68 | // console.log('compositions', compositions); 69 | 70 | return t.any( 71 | `UPDATE compositions SET name = $1, description = $2, 72 | definition = $3 WHERE id = $4 AND userid = $5`, 73 | [ 74 | compositionname, 75 | '', 76 | JSON.stringify(req.body), 77 | compositions[0].id, 78 | userid, 79 | ] 80 | ); 81 | } 82 | }); 83 | }) 84 | .then((data) => { 85 | res.statusCode = 200; 86 | return res.send('Composition Save: Save successful'); 87 | }) 88 | .catch((error) => { 89 | console.log('ERROR: ', error); 90 | res.statusCode = 500; 91 | return res.send( 92 | 'Composition Save: Failure when saving the composition. Error:', 93 | error 94 | ); 95 | }); 96 | }); 97 | 98 | // fs.writeFile(agnosticfilePath, JSON.stringify(req.body), (err) => { 99 | // if (err) { 100 | // console.log('Composition Save: ', err); 101 | // res.statusCode = 500; 102 | // return res.send('Composition Save: Failure when saving the composition'); 103 | // } 104 | // console.log('Composition Save: Save successful'); 105 | // res.statusCode = 200; 106 | // return res.send(''); 107 | // }); 108 | }; 109 | -------------------------------------------------------------------------------- /pages/api/functions/read-functions.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | import db from '../../../data/db'; 4 | import { getSession } from 'next-auth/client'; 5 | 6 | const readFunctionWithFiles = (req, res) => { 7 | const directory = path.join(process.cwd(), 'data/users'); 8 | const filePath = path.join(directory, 'functions.json'); 9 | 10 | const functions = JSON.parse(fs.readFileSync(filePath)); 11 | res.statusCode = 200; 12 | res.send(functions); 13 | // console.log('Functions from read functions: ', functions); 14 | }; 15 | 16 | export default async (req, res) => { 17 | const session = await getSession({ req }); 18 | db.any( 19 | `select functions.* from functions inner join users on 20 | functions.userid = users.id where users.name = $1`, 21 | session.user.name 22 | ) 23 | .then((data) => { 24 | console.log('DATA:', data); // print data; 25 | const functions = {}; 26 | data.forEach((element) => { 27 | functions[element.id] = element; 28 | }); 29 | res.statusCode = 200; 30 | res.send(functions); 31 | }) 32 | .catch((error) => { 33 | console.log('ERROR:', error); // print the error; 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /pages/api/functions/upsert-function.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | import db from '../../../data/db'; 3 | import { getSession } from 'next-auth/client'; 4 | 5 | export const config = { 6 | api: { 7 | bodyParser: { 8 | sizeLimit: '1mb', 9 | }, 10 | }, 11 | }; 12 | 13 | export default async (req, res) => { 14 | // Determine whether we are updating or inserting 15 | // If there is an ID, we're inserting new function 16 | const { name, description, definition, id } = req.body; 17 | const session = await getSession({ req }); 18 | console.log('SESSION NAME: ', session.user.name); 19 | db.task((t) => { 20 | return t 21 | .one( 22 | 'SELECT id FROM users WHERE name = $1', 23 | session.user.name, 24 | (a) => a.id 25 | ) 26 | .then((userid) => { 27 | console.log('USERID: ', userid); 28 | if (!id) { 29 | console.log('no ID'); 30 | return t.any( 31 | `INSERT INTO functions(name, description, definition, userid) 32 | VALUES($1, $2, $3, $4)`, 33 | [name, description, definition, userid] 34 | ); 35 | } else { 36 | console.log('ID exists'); 37 | return t.any( 38 | `UPDATE functions SET name = $1, description = $2, 39 | definition = $3 WHERE id = $4 AND userid = $5`, 40 | [name, description, definition, id, userid] 41 | ); 42 | } 43 | }) 44 | .then((data) => { 45 | res.statusCode = 200; 46 | return res.send('Function added successfully!'); 47 | }) 48 | .catch((error) => { 49 | console.log('ERROR: ', error); 50 | res.statusCode = 500; 51 | return res.send('Error in add-function: ', error); 52 | }); 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default (req, res) => { 4 | res.statusCode = 200; 5 | res.json({ name: 'John Doe' }); 6 | }; 7 | -------------------------------------------------------------------------------- /pages/api/ibm/convert/[compositionName].js: -------------------------------------------------------------------------------- 1 | // import path from 'path' 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const shell = require('shelljs'); 5 | 6 | import db from '../../../../data/db'; 7 | import { 8 | getSession 9 | } from 'next-auth/client'; 10 | 11 | const userDirectory = path.join(process.cwd(), 'data/users'); 12 | const compositionsDirectory = path.join(userDirectory, 'compositions'); 13 | 14 | function getIfComposition(ifAction, successAction, failureAction) { 15 | return `module.exports = composer.if( 16 | composer.action('${ifAction.name}', 17 | { 18 | action: ${ifAction.definition} } 19 | }), 20 | composer.action('${successAction.name}', 21 | { 22 | action: ${successAction.definition} } 23 | }), 24 | composer.action('${failureAction.name}', 25 | { 26 | action: ${failureAction.definition} } 27 | }))`; 28 | } 29 | 30 | function getSequenceComposition(action1, action2) { 31 | return `module.exports = composer.sequence( 32 | composer.action('${action1.name}', 33 | { 34 | action: ${action1.definition} } 35 | }), 36 | composer.action('${action2.name}', 37 | { 38 | action: ${action2.definition} } 39 | }) 40 | )`; 41 | } 42 | 43 | async function ConvertAgnosticCompositionIntoJSCode(compositionName, user) { 44 | 45 | return await db.task((t) => { 46 | return t 47 | .one( 48 | 'SELECT id FROM users WHERE name = $1', 49 | user, 50 | (a) => a.id 51 | ) 52 | .then((userid) => { 53 | return t 54 | .one( 55 | `SELECT definition 56 | FROM compositions 57 | WHERE name=$1 and userid=$2`, 58 | [compositionName, userid], 59 | ({ 60 | definition 61 | }) => ({ 62 | definition, 63 | userid 64 | }) 65 | ) 66 | }) 67 | .then(async ({ 68 | definition, 69 | userid 70 | }) => { 71 | const functions = await t 72 | .any( 73 | `SELECT id, name, description, definition 74 | FROM functions 75 | WHERE userid=$1`, 76 | userid 77 | ); 78 | return { 79 | definition: JSON.parse(definition), 80 | functions, 81 | userid 82 | } 83 | }) 84 | .then(({ 85 | definition, 86 | functions, 87 | userid 88 | }) => { 89 | const functionsObj = {}; 90 | functions.forEach((element) => { 91 | functionsObj[element.id] = element; 92 | }); 93 | let jsContent = "const composer = require('openwhisk-composer'); "; 94 | // if its an "If composition" 95 | if (definition.type == 'ifelse') { 96 | jsContent += getIfComposition({ 97 | name: functionsObj[definition.func[0]].name, 98 | definition: functionsObj[definition.func[0]].definition, 99 | }, { 100 | name: functionsObj[definition.func[1]].name, 101 | definition: functionsObj[definition.func[1]].definition, 102 | }, { 103 | name: functionsObj[definition.func[2]].name, 104 | definition: functionsObj[definition.func[2]].definition, 105 | }, ); 106 | } else if (definition.type == 'sequence') { 107 | jsContent += getSequenceComposition({ 108 | name: functionsObj[definition.func[0]].name, 109 | definition: functionsObj[definition.func[0]].definition, 110 | }, { 111 | name: functionsObj[definition.func[1]].name, 112 | definition: functionsObj[definition.func[1]].definition, 113 | }, ); 114 | } 115 | // if its an "sequence composition" 116 | // TODO: sequence code 117 | // Save the IBM specific JS into a file for later use 118 | const ibmCompositionJsfilePath = path.join(compositionsDirectory, `${compositionName}.js`); 119 | fs.writeFileSync(ibmCompositionJsfilePath, jsContent); 120 | return ibmCompositionJsfilePath; 121 | }) 122 | .catch((error) => { 123 | console.log('ERROR: ', error); 124 | res.statusCode = 500; 125 | return res.send('Error in add-function: ', error); 126 | }); 127 | }); 128 | } 129 | 130 | export default async (req, res) => { 131 | const { 132 | query: { 133 | compositionName 134 | }, 135 | } = req; 136 | const session = await getSession({ 137 | req 138 | }); 139 | // convert from agnostic to ibm specific JS composition representation 140 | const ibmCompositionJsfilePath = await ConvertAgnosticCompositionIntoJSCode(compositionName, session.user.name); 141 | 142 | // convert the JS into JSON IBM specific representation with the compose cmd line 143 | const ibmCompositionJsonfilePath = path.join(compositionsDirectory, `${compositionName}.json`); 144 | const cmd = `compose ${ibmCompositionJsfilePath} > ${ibmCompositionJsonfilePath}`; 145 | shell.exec(cmd, (code, stdout, stderr) => { 146 | console.log('ibmconvert Exit code:', code); 147 | console.log('ibmconvert target output:', stdout); 148 | console.log('ibmconvert target stderr:', stderr); 149 | // check code and stdout and stderr 150 | if (code !== 0) { 151 | res.statusCode = 500; 152 | res.send(stderr); 153 | } else { 154 | res.statusCode = 200; 155 | // return the converted file 156 | const convertedFileContent = fs.readFileSync(`${ibmCompositionJsonfilePath}`); 157 | res.json(convertedFileContent); 158 | } 159 | }); 160 | }; -------------------------------------------------------------------------------- /pages/api/ibm/deploy/[compositionName].js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | const path = require('path'); 3 | 4 | const compositionsDirectory = path.join(process.cwd(), 'data/users/compositions'); 5 | 6 | export default (req, res) => { 7 | // get current user to get its configuration and to know where to get the files from 8 | // get the name of the composition to deploy 9 | const { 10 | query: { compositionName }, 11 | } = req; 12 | const filePath = path.join(compositionsDirectory, compositionName); 13 | const ibmDeployCmd = `deploy ${compositionName} ${filePath}.json -w`; 14 | shell.exec(ibmDeployCmd, (code, stdout, stderr) => { 15 | console.log('ibm function deploy Exit code:', code); 16 | console.log('ibm function deploy stdout:', stdout); 17 | console.log('ibm function deploy stdout:', stdout); 18 | // check code and stdout and stderr 19 | if (code !== 0) { 20 | res.statusCode = 500; 21 | res.send(stderr); 22 | } else { 23 | res.statusCode = 200; 24 | res.send(stdout); 25 | } 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /pages/api/ibm/invoke/[compositionName].js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | 3 | export const config = { 4 | api: { 5 | bodyParser: { 6 | sizeLimit: '1mb', 7 | }, 8 | }, 9 | }; 10 | 11 | 12 | export default (req, res) => { 13 | // get current user to get its configuration and to know where to get the files from 14 | // get the name of the composition to deploy 15 | const { 16 | query: { compositionName }, 17 | } = req; 18 | let ibmInvokeCmd = `ibmcloud fn action invoke --result ${compositionName}`; 19 | // get parameters, if any received in the body 20 | if (req.body != null) { 21 | const { body } = req; 22 | const keys = Object.keys(body); 23 | ibmInvokeCmd += ' --param'; 24 | keys.forEach((key) => { 25 | ibmInvokeCmd += ` ${key} ${body[key]}`; 26 | }); 27 | } 28 | console.log('ibm function invoke cmd', ibmInvokeCmd); 29 | shell.exec(ibmInvokeCmd, (code, stdout, stderr) => { 30 | console.log('ibm function invoke Exit code:', code); 31 | console.log('ibm function invoke stdout:', stdout); 32 | console.log('ibm function invoke stdout:', stdout); 33 | // check code and stdout and stderr 34 | if (code !== 0) { 35 | res.statusCode = 500; 36 | res.send(stderr); 37 | } else { 38 | res.statusCode = 200; 39 | res.send(stdout); 40 | } 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /pages/api/ibm/invoke/detailed/[compositionName].js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | 3 | export const config = { 4 | api: { 5 | bodyParser: { 6 | sizeLimit: '1mb', 7 | }, 8 | }, 9 | }; 10 | 11 | 12 | export default (req, res) => { 13 | // get current user to get its configuration and to know where to get the files from 14 | // get the name of the composition to deploy 15 | const { 16 | query: { compositionName }, 17 | } = req; 18 | let ibmInvokeCmd = `ibmcloud fn action invoke ${compositionName}`; 19 | // get parameters, if any received in the body 20 | if (req.body != null) { 21 | const { body } = req; 22 | const keys = Object.keys(body); 23 | ibmInvokeCmd += ' --param'; 24 | keys.forEach((key) => { 25 | ibmInvokeCmd += ` ${key} ${body[key]}`; 26 | }); 27 | } 28 | console.log('ibm function invoke cmd', ibmInvokeCmd); 29 | shell.exec(ibmInvokeCmd, (code, stdout, stderr) => { 30 | console.log('ibm function invoke Exit code:', code); 31 | console.log('ibm function invoke stdout:', stderr); 32 | console.log('ibm function invoke stderr:', stdout); 33 | // check code and stdout and stderr 34 | if (code !== 0) { 35 | res.statusCode = 500; 36 | res.send(stderr); 37 | } else { 38 | const results = stdout.split(' '); 39 | const resultGuid = results[results.length - 1]; 40 | const ibmInvokeDetailCmd = `ibmcloud fn activation get ${resultGuid}`; 41 | shell.exec(ibmInvokeDetailCmd, (code, stdout, stderr) => { 42 | console.log('ibm function invoke detailed Exit code:', code); 43 | console.log('ibm function invoke detailed stdout:', stdout); 44 | console.log('ibm function invoke detailed stderr:', stderr); 45 | if (code !== 0) { 46 | res.statusCode = 500; 47 | res.send(stderr); 48 | } else { 49 | res.statusCode = 200; 50 | res.json(stdout); 51 | } 52 | }); 53 | } 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /pages/api/ibm/login.js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | 6 | export default (req, res) => { 7 | // get current user to get its configuration and to know where to get the files from 8 | // get its config 9 | const directory = path.join(process.cwd(), 'data/users/'); 10 | const configForUserPath = path.join(directory, 'userconfig.json'); 11 | const configString = fs.readFileSync(configForUserPath, 'utf8'); 12 | const configForUser = JSON.parse(configString); 13 | console.log('user config', configForUser); 14 | const { 15 | region, user, password, resourcegroup, org, space, 16 | } = configForUser.ibmcloud; 17 | const ibmCloudCmd = `ibmcloud login -r '${region}' -u ${user} -p '${password}' -g ${resourcegroup}`; 18 | const ibmCloudTargetCmd = `ibmcloud target -o '${org}' -s ${space}`; 19 | shell.exec(ibmCloudCmd, (code, stdout, stderr) => { 20 | console.log('ibm login Exit code:', code); 21 | console.log('ibm login output:', stdout); 22 | console.log('ibm login stderr:', stderr); 23 | // check code and stdout and stderr 24 | if (code !== 0) { 25 | res.statusCode = 500; 26 | res.send(stderr); 27 | } else { 28 | shell.exec(ibmCloudTargetCmd, (code, stdout, stderr) => { 29 | console.log('ibmcloud target Exit code:', code); 30 | console.log('ibmcloud target output:', stdout); 31 | console.log('ibmcloud target stderr:', stderr); 32 | // check code and stdout and stderr 33 | if (code !== 0) { 34 | res.statusCode = 500; 35 | res.send(stderr); 36 | } else { 37 | res.statusCode = 200; 38 | res.send(stdout); 39 | } 40 | }); 41 | } 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /pages/execution/index.tsx: -------------------------------------------------------------------------------- 1 | import Execution from '../../components/Execution/Execution'; 2 | 3 | const ExecutionPage = (): JSX.Element => ; 4 | 5 | export default ExecutionPage; 6 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Container, Row, Col } from 'react-bootstrap'; 2 | import { signin, signout, useSession } from 'next-auth/client'; 3 | import React, { useState } from 'react'; 4 | // import { useDispatch, useSelector } from 'react-redux'; 5 | import BasicFlow from '../components/FlowChart-dynamic'; 6 | import FlowButtons from '../components/FlowStructureButton'; 7 | 8 | import FunctionInventory from '../components/FunctionInventory'; 9 | import FuncEditor from '../components/FuncEditor'; 10 | import Execution from '../components/Execution/Execution'; 11 | import Nav from '../components/Nav'; 12 | 13 | const Home = (): JSX.Element => { 14 | const [session, loading] = useSession(); 15 | const [flowState, setflowState] = useState(''); 16 | console.log(process.env.NEXTAUTH_URL); 17 | const onSaveClick = async (flow) => { 18 | const res = await fetch( 19 | `http://localhost:3000/api/composition/agnosticsave/${flow.name}`, 20 | { 21 | method: 'post', 22 | headers: { 'Content-Type': 'application/json' }, 23 | body: JSON.stringify(flow), 24 | } 25 | ); 26 | if (res.status === 200) { 27 | console.log('Saved Succesfully'); 28 | setflowState(flow); 29 | } 30 | }; 31 | return ( 32 |
33 |
56 | ); 57 | }; 58 | 59 | export default Home; 60 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/FaaSCompose/090d084823f0b64161bc4d23c1f978a53ebd8b3c/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # must be unique in a given SonarQube instance 2 | sonar.projectKey=jpascas_FaaSCompose 3 | 4 | # --- optional properties --- 5 | 6 | # defaults to project key 7 | # sonar.projectName=FaaSCompose 8 | # defaults to 'not provided' 9 | # sonar.projectVersion=1.0 10 | 11 | # Path is relative to the sonar-project.properties file. Defaults to . 12 | sonar.sources=. 13 | 14 | # Encoding of the source code. Default is default system encoding 15 | #sonar.sourceEncoding=UTF-8 -------------------------------------------------------------------------------- /store/reducers/canvasReducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { StoreState } from '../store'; 3 | 4 | type TActionFlowRendererNodeId = { 5 | payload: string; 6 | type: string; 7 | }; 8 | 9 | const canvasSlice = createSlice({ 10 | name: 'canvas', 11 | initialState: { 12 | flowRendererNodeId: '', 13 | }, 14 | reducers: { 15 | // setNodes: (state, action) => { 16 | // state.flowRendererNodeId = action.payload; 17 | // }, 18 | setFlowRendererNodeId: (state, action: TActionFlowRendererNodeId) => { 19 | state.flowRendererNodeId = action.payload; 20 | }, 21 | // updateNodeName: (state, action) => { 22 | // //will add nodes part later 23 | // // state.nodes = action.payload.tempState; 24 | // // console.log('---updateNodeName--', action); 25 | // // state.nodes[action.payload.key] = { ...action.payload.p }; 26 | // }, 27 | }, 28 | }); 29 | 30 | //export const selectNodes = (state): object => state.canvas.nodes; 31 | export const selectFlowRendererNodeId = (state: StoreState): string => 32 | state.canvas.flowRendererNodeId; 33 | //export const selectNodeName = (state): object => state.canvas.nodes; 34 | 35 | export const { setFlowRendererNodeId } = canvasSlice.actions; 36 | 37 | export default canvasSlice.reducer; 38 | -------------------------------------------------------------------------------- /store/reducers/editorReducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { StoreState } from '../store'; 3 | 4 | const editorSlice = createSlice({ 5 | name: 'editorview', 6 | initialState: { 7 | showEditor: false, 8 | }, 9 | reducers: { 10 | toggleFuncEditor: (state) => { 11 | state.showEditor = !state.showEditor; 12 | }, 13 | }, 14 | }); 15 | 16 | export const selectShow = (state: StoreState): boolean => 17 | state.editor.showEditor; 18 | 19 | export const { toggleFuncEditor } = editorSlice.actions; 20 | 21 | export default editorSlice.reducer; 22 | -------------------------------------------------------------------------------- /store/reducers/executionReducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { StoreState } from '../store'; 3 | 4 | type Action = { 5 | type: string; 6 | payload: string; 7 | }; 8 | 9 | const executionSlice = createSlice({ 10 | name: 'execution', 11 | initialState: { 12 | composition: `{ 13 | "Composition": "Click convert to get your platform specific composition" 14 | }`, 15 | input: '', 16 | output: `{ 17 | "Result": "Click execute to get the result of your platform specific composition" 18 | }`, 19 | compositionName: '', 20 | }, 21 | reducers: { 22 | setComposition: (state, action: Action) => { 23 | state.composition = action.payload; 24 | }, 25 | setUserInput: (state, action: Action) => { 26 | state.input = action.payload; 27 | }, 28 | setCompositionOutput: (state, action: Action) => { 29 | state.output = action.payload; 30 | }, 31 | setCompositionName: (state, action: Action) => { 32 | state.compositionName = action.payload; 33 | }, 34 | }, 35 | }); 36 | 37 | export const selectComposition = (state: StoreState): string => 38 | state.execution.composition; 39 | export const selectUserInput = (state: StoreState): string => 40 | state.execution.input; 41 | export const selectCompositionOutput = (state: StoreState): string => 42 | state.execution.output; 43 | export const selectCompositionName = (state: StoreState): string => 44 | state.execution.compositionName; 45 | 46 | export const { 47 | setComposition, 48 | setUserInput, 49 | setCompositionOutput, 50 | setCompositionName, 51 | } = executionSlice.actions; 52 | 53 | export default executionSlice.reducer; 54 | -------------------------------------------------------------------------------- /store/reducers/functionsReducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { StoreState } from '../store'; 3 | 4 | type TState = { 5 | currentFuncs: TFuncsInventory; 6 | clickedFunc: object; 7 | funcToEdit: TFunc; 8 | }; 9 | 10 | type TFunc = { 11 | id: string; 12 | name: string; 13 | description: string; 14 | definition: string; 15 | }; 16 | export type TFuncsInventory = { 17 | [key: string]: TFunc; 18 | }; 19 | type TActionSetFuncs = { 20 | payload: TFuncsInventory; 21 | type: string; 22 | }; 23 | type TActionAddFunc = { 24 | payload: TFunc; 25 | type: string; 26 | }; 27 | type TActionCurrentFunc = { 28 | payload: string; 29 | type: object; 30 | }; 31 | 32 | const functionsSlice = createSlice({ 33 | name: 'functions', 34 | initialState: { 35 | currentFuncs: {}, 36 | clickedFunc: { id: '', name: '' }, 37 | funcToEdit: { 38 | id: '', 39 | name: 'Name', 40 | description: 'Description', 41 | definition: '// write your function here', 42 | }, 43 | }, 44 | reducers: { 45 | setFuncs: (state: TState, action: TActionSetFuncs) => { 46 | state.currentFuncs = action.payload; 47 | }, 48 | addFunc: (state: TState, action: TActionAddFunc) => { 49 | state.currentFuncs[action.payload.id] = action.payload; 50 | }, 51 | setFuncToEdit: (state: TState, action: TActionAddFunc) => { 52 | state.funcToEdit = action.payload; 53 | }, 54 | setCurrentFunc: (state: TState, action: TActionCurrentFunc) => { 55 | state.clickedFunc = action.payload; 56 | }, 57 | }, 58 | }); 59 | 60 | export const selectFuncs = (state: StoreState): TFuncsInventory => 61 | state.functions.currentFuncs; 62 | export const selectClickedFunc = (state: StoreState): object => 63 | state.functions.clickedFunc; 64 | export const selectFuncToEdit = (state: StoreState): TFunc => 65 | state.functions.funcToEdit; 66 | 67 | export const { 68 | setFuncs, 69 | addFunc, 70 | setCurrentFunc, 71 | setFuncToEdit, 72 | } = functionsSlice.actions; 73 | 74 | export default functionsSlice.reducer; 75 | -------------------------------------------------------------------------------- /store/reducers/sequenceReducer.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { StoreState } from '../store'; 3 | 4 | export type TInitialSequences = { 5 | list: string[]; 6 | clickedSequence: string; 7 | }; 8 | 9 | type TActionInitialSequences = { 10 | payload: TInitialSequences; 11 | type: string; 12 | }; 13 | 14 | type TActionClickedSequence = { 15 | payload: string; 16 | type: string; 17 | }; 18 | 19 | const sequenceSlice = createSlice({ 20 | name: 'sequence', 21 | initialState: { 22 | initialSequences: { 23 | list: ['ifelse', 'sequence'], 24 | clickedSequence: '', 25 | }, 26 | }, 27 | 28 | reducers: { 29 | setInitialSequences: (state, action: TActionInitialSequences) => { 30 | state.initialSequences = action.payload; 31 | }, 32 | changeCurrent: (state, action: TActionClickedSequence) => { 33 | state.initialSequences.clickedSequence = action.payload; 34 | }, 35 | }, 36 | }); 37 | 38 | export const selectSequence = (state: StoreState): TInitialSequences => 39 | state.sequences.initialSequences; 40 | export const selectCurrentSequence = (state: StoreState): string => 41 | state.sequences.initialSequences.clickedSequence; 42 | 43 | export const { setInitialSequences, changeCurrent } = sequenceSlice.actions; 44 | export default sequenceSlice.reducer; 45 | -------------------------------------------------------------------------------- /store/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import executionReducer from './reducers/executionReducer'; 3 | import editorReducer from './reducers/editorReducer'; 4 | import functionsReducer, { TFuncsInventory } from './reducers/functionsReducer'; 5 | import sequenceReducer, { TInitialSequences } from './reducers/sequenceReducer'; 6 | import canvasReducer from './reducers/canvasReducer'; 7 | 8 | export type StoreState = { 9 | execution: { 10 | composition: string; 11 | input: string; 12 | output: string; 13 | compositionName: string; 14 | }; 15 | editor: { 16 | showEditor: boolean; 17 | }; 18 | functions: { 19 | currentFuncs: TFuncsInventory; 20 | clickedFunc: string; 21 | }; 22 | sequences: { 23 | initialSequences: TInitialSequences; 24 | }; 25 | canvas: { 26 | flowRendererNodeId: string; 27 | }; 28 | }; 29 | 30 | export default configureStore({ 31 | reducer: { 32 | execution: executionReducer, 33 | editor: editorReducer, 34 | functions: functionsReducer, 35 | sequences: sequenceReducer, 36 | canvas: canvasReducer, 37 | }, 38 | devTools: true, 39 | }); 40 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | /* html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } */ 17 | 18 | body { 19 | background-color: #fff4ec !important; 20 | } 21 | 22 | #react-flow { 23 | margin-top: 30px !important; 24 | } 25 | .btn-primary { 26 | background-color: #ff784f !important; 27 | border-color: #28151c !important; 28 | color: #28151c !important; 29 | } 30 | 31 | .btn-outline-primary { 32 | background-color: #bccddc !important; 33 | color: #28151c !important; 34 | border-color: #28151c !important; 35 | } 36 | 37 | .icon { 38 | color: #28151c; 39 | font-size: 1.1em; 40 | } 41 | 42 | .icon:hover { 43 | color: #ff784f; 44 | font-weight: 800; 45 | } 46 | -------------------------------------------------------------------------------- /styles/nav.module.css: -------------------------------------------------------------------------------- 1 | .loading, 2 | .loaded { 3 | position: relative; 4 | top: 0; 5 | opacity: 1; 6 | overflow: auto; 7 | border-radius: 0 0 0.6rem 0.6rem; 8 | padding: 0.4rem 0.8rem; 9 | margin: 0; 10 | background-color: #f5f5f5; 11 | transition: all 0.2s ease-in-out; 12 | } 13 | 14 | .loading { 15 | top: -2rem; 16 | opacity: 0; 17 | } 18 | 19 | .signedIn, 20 | .notSignedIn { 21 | position: absolute; 22 | padding: 0.6rem 0 0.4rem 0; 23 | left: 1rem; 24 | right: 7rem; 25 | white-space: nowrap; 26 | text-overflow: ellipsis; 27 | overflow: hidden; 28 | display: inherit; 29 | z-index: 1; 30 | } 31 | 32 | .signedIn { 33 | left: 3.8rem; 34 | } 35 | 36 | .avatar { 37 | border-radius: 2rem; 38 | float: left; 39 | height: 2.2rem; 40 | width: 2.2rem; 41 | background-color: white; 42 | background-size: cover; 43 | border: 2px solid #ddd; 44 | } 45 | 46 | .signinButton, 47 | .signoutButton { 48 | float: right; 49 | margin-right: -0.4rem; 50 | font-weight: 500; 51 | background-color: #1eb1fc; 52 | color: #fff; 53 | border: 1px solid #1eb1fc; 54 | border-radius: 2rem; 55 | cursor: pointer; 56 | font-size: 1rem; 57 | line-height: 1rem; 58 | padding: 0.5rem 1rem; 59 | position: relative; 60 | z-index: 10; 61 | } 62 | 63 | .signinButton:hover { 64 | background-color: #1b9fe2; 65 | border-color: #1b9fe2; 66 | color: #fff; 67 | } 68 | 69 | .signoutButton { 70 | background-color: #fff; 71 | border-color: #bbb; 72 | color: #555; 73 | } 74 | 75 | .signoutButton:hover { 76 | background-color: #fff; 77 | border-color: #aaa; 78 | color: #333; 79 | } 80 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 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 | }, 17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 18 | "exclude": ["node_modules"] 19 | } 20 | --------------------------------------------------------------------------------