diff --git a/package-lock.json b/package-lock.json index b8a61da..4982b4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4132,7 +4132,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4153,12 +4154,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4173,17 +4176,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4300,7 +4306,8 @@ "inherits": { "version": "2.0.4", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4312,6 +4319,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4326,6 +4334,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4333,12 +4342,14 @@ "minimist": { "version": "1.2.5", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.9.0", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4357,6 +4368,7 @@ "version": "0.5.3", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "^1.2.5" } @@ -4418,7 +4430,8 @@ "npm-normalize-package-bin": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "npm-packlist": { "version": "1.4.8", @@ -4446,7 +4459,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -4458,6 +4472,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4535,7 +4550,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -4571,6 +4587,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4590,6 +4607,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4633,12 +4651,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -5106,15 +5126,16 @@ "dev": true }, "handlebars": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.3.tgz", - "integrity": "sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg==", + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", "dev": true, "requires": { + "minimist": "^1.2.5", "neo-async": "^2.6.0", - "optimist": "^0.6.1", "source-map": "^0.6.1", - "uglify-js": "^3.1.4" + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" }, "dependencies": { "source-map": { @@ -5842,6 +5863,11 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, + "js-base64": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.2.tgz", + "integrity": "sha512-Vg8czh0Q7sFBSUMWWArX/miJeBWYBPpdU/3M/DKSaekLMqrqVPaedp+5mZhie/r0lgrcaYBfwXatEew6gwgiQQ==" + }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -6336,9 +6362,9 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, "minimist-options": { @@ -6925,16 +6951,6 @@ "mimic-fn": "^1.0.0" } }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - } - }, "os-browserify": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", @@ -8815,23 +8831,13 @@ } }, "uglify-js": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.7.tgz", - "integrity": "sha512-FeSU+hi7ULYy6mn8PKio/tXsdSXN35lm4KgV2asx00kzrLU9Pi3oAslcJT70Jdj7PHX29gGUPOT6+lXGBbemhA==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.9.0.tgz", + "integrity": "sha512-j5wNQBWaql8gr06dOUrfaohHlscboQZ9B8sNsoK5o4sBjm7Ht9dxSbrMXyktQpA16Acaij8AcoozteaPYZON0g==", "dev": true, "optional": true, "requires": { - "commander": "~2.20.3", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } + "commander": "~2.20.3" } }, "uid-number": { @@ -9364,9 +9370,9 @@ } }, "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", "dev": true }, "worker-farm": { diff --git a/package.json b/package.json index c7e09f9..c9f29f9 100755 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "dependencies": { "axios": "^0.19.2", "babel-preset-es2015": "^6.24.1", + "js-base64": "2.5.2", "moment": "^2.24.0", "prop-types": "^15.7.2", "react": "^16.13.1", diff --git a/packages/models/src/create-api-form-data.model.ts b/packages/models/src/create-api-form-data.model.ts new file mode 100644 index 0000000..2ff5f17 --- /dev/null +++ b/packages/models/src/create-api-form-data.model.ts @@ -0,0 +1,32 @@ +/** + * @license + * Copyright 2017 JBoss Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ApiDesignTemplate} from './api-design-template.model'; + +export class CreateApiFormData { + + type: string; + name: string; + description: string; + template?: ApiDesignTemplate + + constructor() { + this.type = "OpenAPI30"; + this.name = null; + this.description = null; + this.template = null + } +} diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index c27067f..f04336a 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -9,6 +9,7 @@ export * from './bitbucket-team.model'; export * from './codegen-project.model'; export * from './complete-linked-account.model'; export * from './create-linked-account.model'; +export * from './create-api-form-data.model'; export * from './deferred.model'; export * from './editor-user.model'; export * from './github-organization.model'; diff --git a/packages/models/src/new-api.model.ts b/packages/models/src/new-api.model.ts index b235886..47b4156 100644 --- a/packages/models/src/new-api.model.ts +++ b/packages/models/src/new-api.model.ts @@ -17,12 +17,12 @@ export class NewApi { - specVersion: string; + type: string; name: string; description: string; constructor() { - this.specVersion = null; + this.type = null; this.name = ""; this.description = ""; } diff --git a/packages/services/src/api-services/api-services.ts b/packages/services/src/api-services/api-services.ts index 45d7e12..0a2b1d9 100644 --- a/packages/services/src/api-services/api-services.ts +++ b/packages/services/src/api-services/api-services.ts @@ -1,4 +1,4 @@ -import {Api, ApiCollaborator} from "@apicurio/models"; +import {Api, NewApi, ImportApi, ApiCollaborator} from "@apicurio/models"; import {AbstractHubService} from "./hub"; import {ConfigService} from "../config/config.service"; import {IAuthenticationService} from "../authentication/auth.service"; @@ -12,15 +12,29 @@ export class ApisService extends AbstractHubService { private cachedApis: Api[] = null; private cachedCollaborators: ApiCollaborator[] = null; - /** - * Constructor. - * @param authService - * @param config - */ constructor(authService: IAuthenticationService, config: ConfigService) { super(authService, config); } + public createApi(api: NewApi): Promise { + console.info("[ApisService] Creating the API via the hub API"); + + const createApiUrl: string = this.endpoint("designs"); + const options: AxiosRequestConfig = this.options({ "Accept": "application/json", "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }); + + console.info("[ApisService] Creating an API Design: %s", createApiUrl); + return this.httpPostWithReturn(createApiUrl, api, options); + } + + public importApi(api: ImportApi): Promise { + console.info("[ApisService] Importing an API design via the hub API"); + + const importApiUrl: string = this.endpoint("designs"); + const options: AxiosRequestConfig = this.options({ "Accept": "application/json", "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" }); + + console.info("[ApisService] Importing an API Design: %s", importApiUrl); + return this.httpPutWithReturn(importApiUrl, api, options); + } /** * @see ApisService.getCollaborators */ diff --git a/packages/services/src/api-services/hub.ts b/packages/services/src/api-services/hub.ts index ce39546..a98bc58 100644 --- a/packages/services/src/api-services/hub.ts +++ b/packages/services/src/api-services/hub.ts @@ -116,5 +116,50 @@ export abstract class AbstractHubService { }) .catch(error => console.log(error)); // handle error state } + + // (createApiUrl, api, options); + protected httpPostWithReturn(url: string, data: I, options: AxiosRequestConfig, successCallback?: (data: any) => any): Promise { + + const stringify = JSON.stringify(data); + const config: AxiosRequestConfig = {...{ + method: 'post', + url: url, + data: stringify + }, ...options} + + return axios.request(config) + .then(response => { + let data = response; + if(successCallback) { + return successCallback(data); + } + else { + return data; + } + }) + .catch(error => console.log(error)); + } + + protected httpPutWithReturn(url: string, data: I, options: any, successCallback?: (data: any) => any): Promise { + + const stringify = JSON.stringify(data); + const config: AxiosRequestConfig = {...{ + method: 'put', + url: url, + data: stringify + }, ...options} + + return axios.request(config) + .then(response => { + let data = response; + if(successCallback) { + return successCallback(data); + } + else { + return response; + } + }) + .catch(error => console.log(error)); + } } \ No newline at end of file diff --git a/packages/studio/src/app/app.css b/packages/studio/src/app/app.css index a8c40b5..e20ed8c 100644 --- a/packages/studio/src/app/app.css +++ b/packages/studio/src/app/app.css @@ -64,19 +64,6 @@ color: var(--pf-global--Color--100); } -.app-create-api-form { - width: 50%; -} - -.app-form-helper-text { - font-size: var(--pf-global--FontSize--sm); - color: var(--pf-global--Color--200); -} - -.app-form-helper-text-asterisk { - color: var(--pf-global--danger-color--100); -} - .app-import-api-split-layout > .pf-l-split__item { width: 50%; margin-right: var(--pf-global--spacer--xl); diff --git a/packages/studio/src/app/common/appConfig.ts b/packages/studio/src/app/common/appConfig.ts index c65afeb..91bdaf3 100644 --- a/packages/studio/src/app/common/appConfig.ts +++ b/packages/studio/src/app/common/appConfig.ts @@ -1,4 +1,9 @@ -import {ApisService, CurrentUserService, KeycloakAuthenticationService, ConfigService} from '@apicurio/services'; +import { + ApisService, + CurrentUserService, + KeycloakAuthenticationService, + ConfigService +} from '@apicurio/services'; // Initialize services. diff --git a/packages/studio/src/app/pages/api/createApi.css b/packages/studio/src/app/pages/api/createApi.css new file mode 100644 index 0000000..b7662cb --- /dev/null +++ b/packages/studio/src/app/pages/api/createApi.css @@ -0,0 +1,57 @@ +.app-create-api__form-card-text span { + margin-left: var(--pf-global--spacer--sm); +} + +.app-create-api__form-card { + box-shadow: none !important; +} + +.app-create-api__form-card:hover { + box-shadow: none !important; + color: var(--pf-global--primary-color--100); +} + + +.app-create-api__form-card::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + content: ""; + border: var(--pf-global--BorderWidth--sm) solid var(--pf-global--BorderColor--100); +} + +.app-create-api__form-card:hover::after { + border-color: var(--pf-global--primary-color--100); +} + +.app-create-api__form-card-group { + display: grid; + grid-column-gap: var(--pf-global--spacer--sm); + grid-template-columns: 1fr 1fr 1fr; +} + +.app-create-api__form { + width: 100%; +} + +@media screen and (min-width: 768px) { + .app-create-api__form { + width: 50%; + } +} + +.app-form-helper-text { + font-size: var(--pf-global--FontSize--sm); + color: var(--pf-global--Color--200); +} + +.app-form-helper-text-asterisk { + color: var(--pf-global--danger-color--100); +} + +.app-form-helper-text span { + margin-left: 0 !important; + padding-bottom: 0 !important; +} diff --git a/packages/studio/src/app/pages/api/createApi.tsx b/packages/studio/src/app/pages/api/createApi.tsx index 58c79d8..c81d707 100644 --- a/packages/studio/src/app/pages/api/createApi.tsx +++ b/packages/studio/src/app/pages/api/createApi.tsx @@ -1,51 +1,156 @@ -import React from 'react'; -import { Breadcrumb, BreadcrumbItem, Button, Form, FormGroup, TextInput, TextArea, FormSelectOption, FormSelect, ActionGroup, Title, PageSection, PageSectionVariants } from '@patternfly/react-core'; -import '../../app.css' +import React, {useState, useEffect, useRef} from 'react'; +import { + Breadcrumb, + BreadcrumbItem, + Button, + Card, + CardBody, + Form, + FormGroup, + TextInput, + TextArea, + FormSelectOption, + FormSelect, + ActionGroup, + Title, + PageSection, + PageSectionVariants +} from '@patternfly/react-core'; +import '../../app.css'; +import { PlusCircleIcon } from '@patternfly/react-icons'; +import { PficonTemplateIcon } from '@patternfly/react-icons' +import { Services } from './../../common'; +import './createApi.css'; +import { NewApi, ImportApi} from "@apicurio/models"; +import { CreateApiFormData } from '../../../../../models/src/create-api-form-data.model'; +import {Base64} from "js-base64"; -interface CreateApiProps { - isActive: true -} - -interface CreateApiState { - name: string, - description: string, - apiType: string -} +export const CreateApi = () => { -export class CreateApi extends React.Component { - constructor(props: CreateApiProps) { - super(props); - this.state = { - apiType: 'please choose', - description: '', - name: '' - }; - this.handleTextInputChangeName = this.handleTextInputChangeName.bind(this); - this.handleTextInputChangeDescription = this.handleTextInputChangeDescription.bind(this); - this.onChange = this.onChange.bind(this); - } + const apisService = Services.getInstance().apisService; // TO DO: Add more options here - options = [ - { value: 'Please choose', label: 'Please choose', disabled: false }, - { value: 'Open API 3.0.2', label: 'Open API 3.0.2', disabled: false } + const typeOptions = [ + { value: "OpenAPI20", label: "Open API 2.0 (Swagger)", disabled: false }, + { value: 'OpenAPI30', label: 'Open API 3.0.2', disabled: false } ]; - private onChange = (apiType: string) => { - this.setState({apiType}); + const [apiType, setApiType] = useState(typeOptions[1].value); + const [name, setName] = useState(''); + const [description, setDescription] = useState(''); + const [cardSelected, setCardSelected] = useState('card-blank-api'); + const [isNameValid, setIsNameValid] = useState(true); + const [countSubmit, setCountSubmit] = useState(0); + const [isFormValid, setIsFormValid] = useState(false); + + const onChange = (apiType: string) => { + setApiType(apiType); + }; + + const handleTextInputChangeName = (name: string) => { + if(name.length > 0) { + setIsNameValid(true); + } + setName(name); }; - private handleTextInputChangeName = (name: string) => { - this.setState({ name }); + const handleTextInputChangeDescription = (description: string) => { + setDescription(description); }; - private handleTextInputChangeDescription = (description: string) => { - this.setState({ description }); + const onClickCard = event => { + const newSelected = event.currentTarget.id === cardSelected ? null : event.currentTarget.id + setCardSelected(newSelected); }; - render() { - const { name, description } = this.state; - return ( + // On handle submit, check if the form is valid, and call the onCreateApi method + const handleSubmit = (event) => { + event.preventDefault(); + if (isFormValid) { + onCreateApi(event); + } + } + + // Keep count of the first time the submit button is clicked + const updateButtonCount = () => { + setCountSubmit(countSubmit + 1); + } + + // Every component render except the first button submit, set the validation states of the form + useEffect(() => { + if (countSubmit === 0) { + return; + } + else { + if (name === '') { + setIsNameValid(false); + setIsFormValid(false); + } + else { + setIsFormValid(true); + } + } + }); + + const onCreateApi = (eventData: CreateApiFormData) => { + if(!eventData.target.template) { + const newApi: NewApi = new NewApi(); + newApi.type = eventData.target.type.value; + newApi.name = eventData.target.name.value; + newApi.description = eventData.target.description.value; + + console.log("[CreateApiPageComponent] Creating a new (blank) API: " + JSON.stringify(newApi)); + + apisService.createApi(newApi).then(api => { + // TO DO: Set up routes in the app to redirect here + // let link: string[] = ["/", api.id]; + // console.info("[CreateApiPageComponent] Navigating to: %o", link); + // this.router.navigate(link); + }).catch(error => { + console.error("[CreateApiPageComponent] Error creating an API"); + // TO DO: Set up error handling + //this.error(error); + }) + } else { + let importApi: ImportApi = new ImportApi(); + let spec: any = JSON.parse(JSON.stringify(eventData.template.value)); + updateSpec(spec, eventData); + importApi.data = Base64.encode(JSON.stringify(spec)); + + apisService.importApi(importApi).then(updatedApi => { + // TO DO: Set up routes in the app to redirect here + // let link: string[] = [ "/apis", updatedApi.id ]; + // console.info("[CreateApiPageComponent] Navigating to: %o", link); + // this.router.navigate(link); + }).catch( error => { + console.error("[CreateApiPageComponent] Error creating API: %o", error); + // TO DO: Set up error handling + // this.error(error); + }) + } + } + + const updateSpec = (spec: any, eventData: CreateApiFormData): void => { + // OpenAPI20, OpenAPI30, AsyncAPI20, GraphQL + if (eventData.type === 'OpenAPI20') { + spec.swagger = "2.0"; + } else if (eventData.type === 'OpenAPI30') { + spec.openapi = "3.0.2"; + } else if (eventData.type === 'AsyncAPI20') { + spec.asyncapi = "2.0.0"; + } else if (eventData.type == 'GraphQL') { + spec.graphql = "June 2018"; + } + if (!spec.info) { + spec.info = {}; + } + spec.info.title = eventData.name; + if (eventData.description) { + spec.info.description = eventData.description; + } + } + + return ( @@ -63,47 +168,51 @@ export class CreateApi extends React.Component { -
+

Fields marked with * are required.