├── .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 | 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 |
34 | {session && (
35 |
36 |
37 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | )}
55 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------