<!-- eslint-disable --> <template> <v-container fluid class="api-client grid-list-xs pa-0" style="height: 100%"> <splitpanes style="height: 100%" class="xc-theme"> <pane min-size="20" max-size="50" size="30" style="overflow: auto"> <!-- <p class="body-2 my-1 text-center grey--text text--lighten-1" v-show="!isDashboard">--> <!-- {..} REST API CLIENT</p>--> <v-row class="pa-0 ma-0 pa-2 pl-2"> <div class="cursor-pointer d-flex" style="width: 100%"> <x-icon v-ge="['api-client', 'open-new-collection']" icon-class="mr-1 cursor-pointer" small color="primary" tooltip="Create New API Collection" @click="openNewCollection('/')" > mdi-folder-plus-outline </x-icon> <!-- <x-icon class="mr-1" color="primary" tooltip="Open API Collection" @click="openApiFileCollection">--> <!-- mdi-folder-open-outline--> <!-- </x-icon>--> <v-dialog v-model="importCollection.modal" persistent max-width="550"> <template #activator="{ on }"> <v-icon color="" v-on="on"> mdi-import </v-icon> </template> <v-card class="pa-3" style="position: relative"> <v-icon v-ge="['api-client', 'open-new-collection-close']" style="position: absolute; right: 20px; top: 20px" @click="importCollection.modal = false" > mdi-close </v-icon> <v-card-title class="headline justify-center mb-2"> Import </v-card-title> <v-card-subtitle class="text-center"> (JSON Format Only) </v-card-subtitle> <div style="" class="py-3"> <v-card-text> <v-select v-model="importCollection.type" dense hide-details outlined :items="importCollection.types" filled label="API Specification" /> </v-card-text> <v-card-actions> <v-tabs background-color="indigo accent-4" dark style="border: 1px solid indigo; margin: 0 10px"> <v-tab>FILE</v-tab> <v-tab :disabled="!isSwaggerImport"> URL </v-tab> <v-tab :disabled="!isSwaggerImport"> JSON </v-tab> <v-tab-item> <div class="px-2 py-10 text-center" style="min-height: 250px"> <v-row> <v-col> <v-text-field v-model="importCollection.file.srcFilePath" hide-details label="Select file" dense outlined > <template #append> <v-btn v-ge="['api-client', 'select-import-file']" small @click="selectImportFile()"> Choose File ... </v-btn> </template> </v-text-field> </v-col> </v-row> <v-row v-show="isSwaggerImport"> <v-col> <v-text-field v-model="importCollection.file.dstFilePath" hide-details label="Destination file path" dense outlined hide-details /> </v-col> </v-row> <v-btn v-ge="['api-client', 'import-collection']" class="mt-4" color="primary" @click=" importFile( importCollection.type, importCollection.file.srcFilePath, importCollection.file.dstFilePath ) " > Import </v-btn> </div> </v-tab-item> <v-tab-item> <div style="min-height: 250px" class="text-center"> <div class="px-2 pt-10"> <v-text-field v-model="importCollection.url" placeholder="Enter url" /> </div> <v-btn v-ge="['api-client', 'import-collection-from-url']" color="primary" @click="importFromUrl(importCollection.type, importCollection.url)" > Import </v-btn> </div> </v-tab-item> <v-tab-item> <div style="min-height: 250px" class="text-center"> <div class="px-2 pt-2"> <v-textarea v-model="importCollection.text" placeholder="Enter JSON String" /> </div> <v-btn v-ge="['api-client', 'import-collection-from-text']" color="primary" @click="importFromText(importCollection.type, importCollection.text)" > Import </v-btn> </div> </v-tab-item> </v-tabs> </v-card-actions> </div> </v-card> </v-dialog> <v-spacer /> <span v-show="!isDashboard" class="flex-shrink-1 body-2 my-1 text-center grey--text"> {..} REST API CLIENT ( is in <v-icon small color="warning">mdi-alpha</v-icon>)</span > <span v-show="isDashboard" class="caption float-right warning--text mt-1" style="font-style: italic"> API Client is in<v-icon small color="warning">mdi-alpha</v-icon> </span> </div> </v-row> <v-divider /> <v-tabs v-model="apiRootTab" height="32" style="border-top: 1px solid darkgrey"> <v-tab style="text-transform: none"> History </v-tab> <v-tab style="text-transform: none"> Collections </v-tab> <v-tab-item style="border-top: 1px solid grey"> <div class="apis-list"> <div v-for="(api, i) in historyList" :key="i" class="pa-0 ma-0"> <v-list-item v-ge="['api-client', 'history']" dense two-line @click="apiClickedOnHistoryList(api)"> <v-hover v-slot="{ hover }"> <v-list-item-content> <v-list-item-title class="grey--text"> <v-btn class="pl-0 ml-0" small text :tooltip="api.meta.path" :color="apiMethodMeta[api.meta.method.toUpperCase()].color" > <b> <v-icon class="mx-0 ml-n2" :class="{ 'white--text': !api.meta.response || !api.meta.response.status, 'red--text': api.meta.response && api.meta.response.status >= 400, 'green--text': api.meta.response && api.meta.response.status < 400, }" > mdi-circle-small </v-icon> {{ api.meta.method }} </b> </v-btn> {{ api.meta.path }} </v-list-item-title> <v-list-item-subtitle v-show="!i || hover" class="text-right"> <span v-show="!i && !hover" class="grey--text text--darken-1 caption">(Last invoked API) </span> <v-icon v-if="_isDev" v-ge="['api-client', 'node-info']" small @click.stop="showNodeInfo(api)"> mdi-information </v-icon> <v-btn v-show="hover" v-ge="['api-client', 'delete-list']" small text class=" " @click="apiDeleteFromList(i)" > <v-icon small> mdi-delete </v-icon> </v-btn> </v-list-item-subtitle> </v-list-item-content> </v-hover> </v-list-item> <v-divider /> </div> </div> </v-tab-item> <v-tab-item style="border-top: 1px solid grey"> <v-expansion-panels v-model="curApiCollectionPanel" accordion focusable> <v-expansion-panel v-for="(apiTv, i) in apiTvs" :key="i"> <v-expansion-panel-header hide-actions> <template #default="{ open }"> <div class="d-flex"> <v-icon color=""> {{ open ? 'mdi-menu-down' : 'mdi-menu-right' }} </v-icon> <v-icon small color="grey" class="ml-1 mr-2"> mdi-folder </v-icon> <span class="body-2 flex-grow-1">{{ $store.getters['apiClientSwagger/GtrCurrentApiFilePaths'][i].fileName }}</span> <x-icon color="white grey" class="float-right mr-3" small @click="(showCtxMenu[i] = true), (x = $event.clientX), (y = $event.clientY)" @click.stop="" > mdi-dots-horizontal </x-icon> <recursive-menu v-model="showCtxMenu[i]" v-ge="['api-client', 'collection-context-menu']" :position-x="x" :position-y="y" :items="{ 'Add Folder': 'add-folder', 'Add Request': 'add-request', 'Reveal in Folder': 'reveal-in-folder', 'Delete Collection': 'delete-collection', 'Refresh Collection': 'refresh-collection', }" @click="ctxMenuClickHandler($event, i)" /> </div> </template> </v-expansion-panel-header> <v-expansion-panel-content class="expansion-wrap-0 pl-4"> <vue-tree-list v-if="apiTvs[i]" style="cursor: pointer" class="body-2 sql-query-treeview px-1 pt-2 api-treeview" :model="apiTv" default-tree-node-name="new node" default-leaf-node-name="new leaf" :default-expanded="false" @click="tvNodeOnClick" @change-name="tvNodeRename" @delete-node="tvNodeDelete" @add-node="onAddNode" > <span slot="leafNodeIcon" /> <v-icon slot="treeNodeIcon" small color="grey" class="mr-1"> mdi-folder-star </v-icon> <v-icon slot="addTreeNode" small> mdi-folder-plus </v-icon> <v-icon slot="addLeafNode" small> mdi-file-plus </v-icon> <v-icon slot="editNode" small class="mt-n1"> mdi-file-edit </v-icon> <v-icon slot="delNode" small> mdi-delete </v-icon> <template #label="{ item: api }"> <div v-if="api.isLeaf" class="d-flex pa-1 ma-n1" style="width: 100%" :style=" api.id === currentApi.id && !currentApi.meta.history ? 'background:rgb(240, 240, 240)' : '' " > <!-- <v-icon class="mx-0"--> <!-- :class="`${apiMeta[api.method].color}--text`"--> <!-- small>--> <!-- mdi-bookmark-outline--> <!-- </v-icon>--> <span style="display: inline-block; min-width: 45px" :class=" apiMethodMeta[api.meta.method.toUpperCase()] ? `${apiMethodMeta[api.meta.method.toUpperCase()].color}--text` : '' " > {{ api.meta.method.toUpperCase() === 'DELETE' ? 'DEL' : api.meta.method.toUpperCase() }} </span> <span class="grey--text d-block text--darken-1" style="text-overflow: ellipsis; overflow: hidden; white-space: nowrap" >{{ api.name }}</span > <v-spacer /> <v-icon v-if="_isDev" v-ge="['api-client', 'node-info']" small @click.stop="showNodeInfo(api)"> mdi-information </v-icon> </div> <div v-else class="d-flex" style="width: 100%; text-overflow: ellipsis; overflow: hidden; white-space: nowrap" > <span>{{ api.name }}</span> <v-spacer /> <v-icon v-if="_isDev" v-ge="['api-client', 'node-info']" small @click.stop="showNodeInfo(api)"> mdi-information </v-icon> </div> </template> </vue-tree-list> </v-expansion-panel-content> </v-expansion-panel> </v-expansion-panels> </v-tab-item> </v-tabs> </pane> <pane min-size="10" size="70" style="overflow: auto"> <v-toolbar class="toolbar-border-bottom elevation-0 d-flex req-inputs" height="55" style="position: relative; z-index: 2; width: 100%" > <v-select v-model="api.method" v-ge="['api-client', 'method']" :items="Object.keys(apiMethodMeta)" dense solo hide-details outlined class="body-2" style=" max-width: 130px; border-bottom-right-radius: 0; border-top-right-radius: 0; border-right: 1px solid grey; " /> <xAutoComplete v-model="api.path" outlined class="flex-grow-1" :env="selectedEnv" placeholder="Enter HTTP URL" solo dense hide-details autofocus styles="border-bottom-left-radius: 0;border-top-left-radius:0 " /> <x-btn v-ge="['api-client', 'api-send']" btn.class="primary" dense tooltip="Send Request" @click.prevent="apiSend()" > <v-icon v-if="isPerfFilled" small class="ml-n3"> mdi-truck-fast </v-icon> <v-icon v-else small class="ml-n3"> mdi-send </v-icon> SEND </x-btn> <x-btn v-ge="['api-client', 'save']" btn.class="outlined" dense :icon="api.history ? 'save' : 'mdi-content-save-edit'" tooltip="Save API" @click.prevent="saveOrUpdateApi(api)" /> <x-btn v-ge="['api-client', 'environment']" icon="mdi-eye-outline" tooltip="Environments" @click="environmentDialog = true" /> </v-toolbar> <splitpanes horizontal style="height: calc(100% - 64px)" class="xc-theme"> <pane min-size="25" size="50" style="overflow: auto" class="pa-1"> <v-tabs class="req-tabs" height="24"> <v-tab v-ge="['api-client', 'params']" class="caption"> Params <b v-if="paramsCount" class="green--text">({{ paramsCount }})</b> </v-tab> <v-tab v-ge="['api-client', 'headers']" class="caption"> Headers <b v-if="headersCount" class="green--text">({{ headersCount }})</b> </v-tab> <v-tab v-ge="['api-client', 'body']" class="caption"> Body </v-tab> <v-tab v-ge="['api-client', 'auth']" class="caption"> Auth </v-tab> <v-tab v-ge="['api-client', 'perf-test']" class="caption"> Perf Test </v-tab> <div class="flex-grow-1 d-flex text-right pr-4 justify-end"> <div class="flex-shrink-1"> <v-select v-model="selectedEnv" height="19" class="caption envs" dense :items="environmentList" placeholder="Environment" single-line > <template #selection="{ item }"> <span style="text-transform: uppercase">{{ item }}</span > <span class="grey--text">(env)</span> </template> </v-select> </div> </div> <!-- <div class="flex-grow-1 text-right pr-4 caption" v-if="api.response">--> <!-- <!– <x-icon iconClass="mr-4" v-if="$store.getters['project/GtrProjectJson']" @click="environmentDialog = true" tooltip="Environments">–>--> <!-- <!– mdi-eye-outline–>--> <!-- <!– </x-icon>–>--> <!-- </div>--> <v-tab-item> <params v-model="api.parameters" :env.sync="selectedEnv" /> </v-tab-item> <v-tab-item> <headers v-model="api.headers" :env.sync="selectedEnv" /> </v-tab-item> <v-tab-item> <monaco-json-editor v-model="api.body" style="height: 250px" class="editor card" theme="vs-dark" lang="json" :options="{ validate: true, documentFormattingEdits: true, foldingRanges: true }" /> </v-tab-item> <v-tab-item> <!-- <monaco-editor--> <!-- :code.sync="api.auth"--> <!-- cssStyle="height:250px"></monaco-editor>--> </v-tab-item> <v-tab-item> <perf-test v-model="api.perf" /> </v-tab-item> </v-tabs> </pane> <pane min-size="25" size="50" style="overflow: auto" class="pa-1"> <!-- <h3 class="mb-2 grey--text lighten-1">--> <!-- Response Body :--> <!-- <div v-if="api.response">--> <!-- <span v-if="api.response.status === 200" class="green--text">{{api.response.status}}</span>--> <!-- <span v-if="api.response.status !== 200" class="red--text">{{api.response.status}}</span>--> <!-- </div>--> <!-- </h3>--> <v-tabs v-if="api.response" height="24"> <v-tab v-ge="['api-client', 'response-body']" class="caption"> Body </v-tab> <v-tab v-ge="['api-client', 'respomse-headers']" class="caption"> Headers<span v-if="api.response.headers" class="green--text" >( {{ Object.keys(api.response.headers).length }} )</span > </v-tab> <div v-if="api.response" class="flex-grow-1 text-right pr-4 caption"> <template v-if="api.response.status"> <span class="grey--text">Status:</span ><span :class="{ 'green--text': api.response.status === 200, 'red--text': api.response.status !== 200, }" ><b>{{ api.response.status }}</b></span > </template> <template v-if="api.timeTaken"> <span class="grey--text">Time:</span ><span class="green--text" ><b>{{ api.timeTaken }}ms</b></span > </template> </div> <v-tab-item> <pre v-if="api.response" class="black pa-1 grey--text w-100 caption" style="overflow-x: auto; min-height: 100px; overflow-y: auto; min-width: 100%" >{{ api.response.data }}</pre > <!-- <pre v-if="api.response" class="black pa-1" style="overflow-x: auto;min-height:50px;overflow-y:auto">{{api.response.data}}</pre>--> </v-tab-item> <v-tab-item> <pre v-if="api.response" class="black pa-1 grey--text w-100 caption" style="overflow-x: auto; min-height: 100px; overflow-y: auto; min-width: 100%" >{{ api.response.headers }}</pre > </v-tab-item> </v-tabs> </pane> </splitpanes> </pane> </splitpanes> <environment v-model="environmentDialog" env="_noco" /> <v-dialog v-model="bookmarkApiDialog" max-width="500"> <v-card> <v-card-title class="justify-center"> Bookmark API </v-card-title> <v-card-text> <div class="text-right"> <v-btn v-ge="['api-client', 'open-new-collection']" small color="primary" outlined @click="openNewCollection('/')" > <v-icon small> mdi-plus </v-icon> New Collection </v-btn> </div> <v-list dense> <v-list-item-group v-model="choosenApiCollection"> <v-list-item v-for="(item, i) in $store.getters['apiClientSwagger/GtrCurrentApiFilePaths']" :key="i" :value="i" style="border: 1px solid grey" > <v-list-item-title>{{ item.fileName }}</v-list-item-title> </v-list-item> </v-list-item-group> </v-list> </v-card-text> <v-card-actions class="justify-center pb-5"> <v-btn v-ge="['api-client', 'bookmark']" small @click="bookmarkApiDialog = false"> <!-- Cancel --> {{ $t('general.cancel') }} </v-btn> <v-btn small color="primary" :disabled="choosenApiCollection === null || choosenApiCollection === undefined" @click="bookmarkApi(choosenApiCollection)" > Save API </v-btn> </v-card-actions> </v-card> </v-dialog> </v-container> </template> <script> /* eslint-disable */ import { mapGetters } from 'vuex'; import Vue from 'vue'; import { VueTreeList, Tree, TreeNode } from 'vue-tree-list'; import { Splitpanes, Pane } from 'splitpanes'; import params from '../apiClient/Params'; import headers from '../apiClient/Headers'; import { MonacoJsonEditor } from '../monaco/index'; import environment from '../Environment'; import PerfTest from '../apiClient/PerfTest'; // const {app, dialog, path, fs, shell, XcApis} = require("electron").remote.require( // "./libs" // ); import * as XcApiHelp from '../../helpers/XcApiHelp'; // const {config} = require('electron').remote.require('./libs'); export default { components: { PerfTest, MonacoJsonEditor, VueTreeList, Splitpanes, Pane, params, headers, environment, }, data() { return { choosenApiCollection: null, bookmarkApiDialog: false, apiRootTab: 0, importCollection: { modal: false, type: 'Swagger 3.0', types: [ { text: 'Swagger 3.0', value: 'Swagger 3.0', }, { text: 'XC', value: 'XC', }, ], file: { srcFilePath: '', dstFilePath: '', }, }, fileHistory: null, historyList: [], environmentDialog: false, showCtxMenu: {}, apiTvs: [], apiFilePaths: [], apiFileCollections: [], curApiCollectionPanel: null, x: 0, y: 0, apiMethodMeta: { GET: { color: 'success', }, POST: { color: 'warning', }, DELETE: { color: 'error', }, PUT: { color: 'info', }, HEAD: { color: 'info', }, PATCH: { color: 'info', }, }, // current api api: { method: 'GET', path: '', body: '', params: [], auth: '', headers: [], response: {}, perf: {}, meta: {}, }, currentApi: {}, currentNode: null, }; }, computed: { isPerfFilled() { return this.api.perf && Object.values(this.api.perf).some(v => v); }, ...mapGetters({ sqlMgr: 'sqlMgr/sqlMgr', currentProjectFolder: 'project/currentProjectFolder', projectApisFolderPath: 'project/projectApisFolderPath', projectApisFolder: 'project/projectApisFolder', }), isSwaggerImport() { return this.importCollection.type === 'Swagger 3.0'; }, paramsCount() { return this.api.parameters && this.api.parameters.filter(p => p.name && p.enabled).length; }, headersCount() { return this.api.headers && this.api.headers.filter(h => h.name && h.enabled).length; }, environmentList() { if (this.isDashboard) { return Object.keys(this.$store.getters['project/GtrApiEnvironment']); } else { return Object.keys(this.$store.getters['project/GtrDefaultApiEnvironment']); } }, selectedEnv: { get() { return ( this.$store.state.apiClientSwagger.activeEnvironment[this.$store.state.apiClientSwagger.currentProjectKey] || this.environmentList[0] ); }, set(env) { this.$store.commit('apiClientSwagger/MutActiveEnvironment', { env }); }, }, }, watch: { async historyList(newData) { await this.fileHistory.write({ path: config.electron.apiHistoryPath, data: newData, }); }, }, async created() { try { /* load history collection - default path is within app directory */ await this.loadHistoryFile(); if (this.$route.params && this.$route.params.project) { /* get collection paths previously opened for this project */ this.$store.dispatch('apiClientSwagger/loadApiCollectionForProject', { projectId: this.$route.params.project, projectName: this.$store.getters['project/GtrProjectName'], }); } else { /* get collection paths previously opened */ this.$store.dispatch('apiClientSwagger/loadApiCollectionForProject', {}); } /* load the filecollection json as treeviews */ for (let i = 0; i < this.$store.getters['apiClientSwagger/GtrCurrentApiFilePaths'].length; ++i) { await this.loadFileCollection(this.$store.getters['apiClientSwagger/GtrCurrentApiFilePaths'][i]); } } catch (e) { console.log('Failed to load previously opened query collections', e); } if (this.nodes && this.nodes.url) { Vue.set(this.api, 'method', 'GET'); this.api.path = this.nodes.url; } this.api.meta = {}; try { const info = ( await this.$axios.get('/nc/projectApiInfo', { headers: { 'xc-auth': this.$store.state.users.token, }, }) ).data; const rest = Object.values(info).find(v => v.apiUrl); if (rest) { Vue.set(this.api, 'method', 'GET'); Vue.set(this.api, 'path', rest.apiUrl); } } catch (e) {} }, mounted() {}, beforeDestroy() {}, methods: { showNodeInfo(api) { console.log('Node info : ', api); }, async handleKeyDown({ metaKey, key, altKey, shiftKey, ctrlKey }) { // cmd + s -> save // cmd + l -> reload // cmd + n -> new // cmd + d -> delete // cmd + enter -> send api switch ([metaKey, key].join('_')) { case 'true_s': await this.bookmarkApi(); break; case 'true_e': this.environmentDialog = true; break; // case 'true_n' : // this.addColumn(); // break; case 'true_d': await this.deleteProcedure('showDialog'); break; case 'true_Enter': await this.apiSend(); break; } }, async openCollectionFolder(pathString) { shell.showItemInFolder(pathString); }, async openNewCollection() { try { const toLocalPath = this.isDashboard ? path.join(this.currentProjectFolder, 'server', 'tool', this.projectApisFolder) : ''; const userChosenPath = dialog.showSaveDialog({ defaultPath: toLocalPath, filters: [{ name: 'JSON', extensions: ['json'] }], }); if (userChosenPath) { fs.writeFileSync(userChosenPath, '[]', 'utf-8'); const pathObj = { path: userChosenPath, fileName: path.basename(userChosenPath), }; this.$store.commit('apiClientSwagger/MutApiFilePathsAdd', pathObj); await this.loadFileCollection(pathObj); } this.$toast.success('New API collection loaded successfully').goAway(5000); } catch (e) { console.log(e); throw e; } }, async openSwaggerJSONFile(srcPath, dstPath) { const dstFileName = path.basename(dstPath); await XC.importSwaggerJson({ srcPath, dstPath, }); const pathObj = { path: dstPath, fileName: dstFileName, }; this.$store.commit('apiClientSwagger/MutApiFilePathsAdd', pathObj); await this.loadFileCollection(pathObj); }, selectImportFile() { const file = dialog.showOpenDialog({ properties: ['openFile'], }); const dstFileName = `${path.basename(file[0], '.json')}.xc.json`; let dstPath = ''; if (this.isDashboard) { dstPath = this.projectApisFolderPath; } else { dstPath = path.dirname(config.electron.apiHistoryPath); } if (file && file[0]) { this.$set(this.importCollection.file, 'srcFilePath', file[0] + ''); this.$set(this.importCollection.file, 'dstFilePath', path.join(dstPath, dstFileName)); } }, async importFile(type, srcPath, dstPath) { this.apiRootTab = 1; this.$set(this.importCollection, 'modal', false); try { if (type === 'Swagger 3.0') { await this.openSwaggerJSONFile(srcPath, dstPath); } else { await this.openApiFileCollection(srcPath, dstPath); } this.$toast.success('File imported successfully').goAway(3000); } catch (e) { console.log('File import error : ', e); this.$toast.error('File importing failed').goAway(3000); } }, async importFromUrl(type, url) { this.apiRootTab = 1; this.$set(this.importCollection, 'modal', false); try { if (type === 'Swagger 3.0') { await this.importSwaggerFromUrl(url); } else { await this.importXcFromUrl(url); } this.$toast.success('Import from URL completed successfully').goAway(3000); } catch (e) { console.log('URL import error : ', e); this.$toast.error('Import from URL failed').goAway(3000); } }, async importFromText(type, text) { this.apiRootTab = 1; this.$set(this.importCollection, 'modal', false); try { if (type === 'Swagger 3.0') { await this.importSwaggerFromText(text); } else { await this.importXcFromText(text); } this.$toast.success('Import from Text completed successfully').goAway(3000); } catch (e) { console.log('Text import error : ', e); this.$toast.error('Import from Text failed').goAway(3000); } }, async importSwaggerFromUrl(url) { const data = await this.$axios.$get(url); const dstFileName = `${Date.now()}.xc.json`; let dstPath = ''; if (this.isDashboard) { dstPath = this.projectApisFolderPath; } else { dstPath = path.dirname(config.electron.apiHistoryPath); } await XC.importSwaggerObject({ swagger: data, dstPath: path.join(dstPath, dstFileName), }); const pathObj = { path: path.join(dstPath, dstFileName), fileName: dstFileName, }; this.$store.commit('apiClientSwagger/MutApiFilePathsAdd', pathObj); await this.loadFileCollection(pathObj); }, async importSwaggerFromText(text) { const data = JSON.parse(text); const dstFileName = `${Date.now()}.xc.json`; let dstPath = ''; if (this.isDashboard) { dstPath = this.projectApisFolderPath; } else { dstPath = path.dirname(config.electron.apiHistoryPath); } await XC.importSwaggerObject({ swagger: data, dstPath: path.join(dstPath, dstFileName), }); const pathObj = { path: path.join(dstPath, dstFileName), fileName: dstFileName, }; this.$store.commit('apiClientSwagger/MutApiFilePathsAdd', pathObj); await this.loadFileCollection(pathObj); this.$toast.success('Import from Text completed successfully').goAway(3000); }, async importXcFromUrl(url) { const data = await this.$axios.$get(url); const dstFileName = `${Date.now()}.xc.json`; let dstPath = ''; if (this.isDashboard) { dstPath = this.projectApisFolderPath; } else { dstPath = path.dirname(config.electron.apiHistoryPath); } // await XcApis.importSwaggerObject({ // swagger: data, // dstPath: path.join(dstPath, dstFileName) // }) const pathObj = { path: path.join(dstPath, dstFileName), fileName: dstFileName, }; this.$store.commit('apiClientSwagger/MutApiFilePathsAdd', pathObj); await this.loadFileCollection(pathObj); }, async importXcFromText(text) { const data = JSON.parse(text); const dstFileName = `${Date.now()}.xc.json`; let dstPath = ''; if (this.isDashboard) { dstPath = this.projectApisFolderPath; } else { dstPath = path.dirname(config.electron.apiHistoryPath); } // // await XcApis.importSwaggerObject({ // swagger: data, // dstPath: path.join(dstPath, dstFileName) // }) const pathObj = { path: path.join(dstPath, dstFileName), fileName: dstFileName, }; this.$store.commit('apiClientSwagger/MutApiFilePathsAdd', pathObj); await this.loadFileCollection(pathObj); this.$toast.success('Import from Text completed successfully').goAway(3000); }, async ctxMenuClickHandler(actionEvent, index) { switch (actionEvent.value) { case 'add-folder': this.tvNodeFolderAdd(index); break; case 'add-request': this.curApiCollectionPanel = index; this.tvNodeRequestAdd(index); break; case 'reveal-in-folder': await this.openCollectionFolder(this.$store.getters['apiClientSwagger/GtrCurrentApiFilePaths'][index].path); break; case 'delete-collection': this.$store.commit('apiClientSwagger/MutApiFilePathsRemove', index); this.apiFileCollections.splice(index, 1); this.apiTvs.splice(index, 1); break; case 'refresh-collection': await this.refreshFileCollection(index); break; default: break; } // this.deleteQueryByPath(this.apiTvs, this.menuItem.path); }, openUrl(url) { shell.openExternal(url); }, apiClickedOnList(api) { if (api.isLeaf) { api = XcApiHelp.apiPrepareForInvocation(api); this.currentApi = JSON.parse(JSON.stringify(api)); this.api = { perf: {}, ...this.currentApi.meta, }; } }, apiClickedOnHistoryList(api) { this.currentApi = JSON.parse(JSON.stringify(api)); this.api = { perf: {}, ...api.meta, history: true, isLeaf: true, }; }, apiDeleteFromList(index) { this.$store.commit('apiClientSwagger/MutListRemove', index); }, async apiSend() { if (!this.api.path.trim()) { this.$toast.info('Please enter http url').goAway(3000); return; } const envs = this.isDashboard ? this.$store.getters['project/GtrApiEnvironment'][this.selectedEnv] : this.$store.getters['project/GtrDefaultApiEnvironment'][this.selectedEnv]; const apiDecoded = JSON.parse( JSON.stringify(this.api).replace(/{{\s*(\w+)\s*}}/g, (m, m1) => { if (m1 in envs) { return envs[m1]; } else { this.$toast.info('Environment variable is not found : ' + m1).goAway(3000); return m; } }) ); const swaggerResult = await this.$store.dispatch('apiClientSwagger/send', { api: this.api, apiDecoded }); this.historyList = [{ ...this.currentApi, meta: swaggerResult, pid: 0 }, ...this.historyList]; const result = swaggerResult.response; this.api = swaggerResult; if (result) { if (result.status === 200) { // this.$toast.success('API invoked successfully',{duration:1000}); } else { this.$toast.error(`Error:${result.status}`, { duration: 1000 }); } } else { this.$toast.error('Some internal error occurred', { duration: 1000 }); } }, async fileCollectionReload() { const data = new Tree(await this.apiFileCollection.read()); this.apiTv = data; // this.$set(this, 'apiCollections', data); }, async tvNodeDelete(node) { console.log(node, this.curApiCollectionPanel); node.remove(); await this.savefileCollections(this.curApiCollectionPanel); }, async tvNodeRename(params) { await this.savefileCollections(this.curApiCollectionPanel); }, async onAddNode(params) { await this.savefileCollections(this.curApiCollectionPanel); }, async tvNodeOnClick(node) { const { parent, children, ...params } = node; this.currentNode = node; this.apiClickedOnList(params); // if (params.query) ; // this.selectQuery(params) }, async savefileCollections(index = 0) { await this.apiFileCollections[index].write({ data: this.tvToObject(index) }); // this.apiTvs = await this.fileCollections.read(); // this.$set(this.apiTvs, index, this.apiTvs); }, tvToObject(index) { try { const vm = this; function _dfs(oldNode) { const newNode = {}; XcApiHelp.nodeHandleIfNew(oldNode); for (const k in oldNode) { if (k !== 'children' && k !== 'parent') { newNode[k] = oldNode[k]; } } if (oldNode.children && oldNode.children.length > 0) { newNode.children = []; for (let i = 0, len = oldNode.children.length; i < len; i++) { newNode.children.push(_dfs(oldNode.children[i])); } } return newNode; } return _dfs(vm.apiTvs[index]).children; } catch (e) { this.$toast.error('Error while parsing api collection').goAway(3000); } }, async tvNodeFolderAdd(index) { const node = new TreeNode({ name: 'New Folder', isLeaf: false, children: [] }); if (!this.apiTvs[index].children) { this.apiTvs[index].children = []; } this.apiTvs[index].addChildren(node); await this.saveFileCollection(index); }, async tvNodeRequestAdd(index) { const node = new TreeNode({ name: 'New Request', isLeaf: true }); if (!this.apiTvs[index].children) { this.apiTvs[index].children = []; } this.apiTvs[index].addChildren(node); await this.saveFileCollection(index); }, openApiFileCollection(filePath = null) { const vm = this; // console.log(obj, key); const file = filePath ? [filePath] : dialog.showOpenDialog({ properties: ['openFile'], }); if (file && file[0]) { const fileName = path.basename(file[0]); const pathObj = { path: file[0] + '', fileName, }; if (this.$store.getters['apiClientSwagger/GtrCurrentApiFilePaths'].every(({ path: p }) => p !== pathObj.path)) { this.$store.commit('apiClientSwagger/MutApiFilePathsAdd', pathObj); this.loadFileCollection(pathObj); } else { this.$toast.info('File already exist in collection').goAway(4000); } } }, async saveFileCollection(index = 0) { await this.apiFileCollections[index].write({ data: this.tvToObject(index) }); }, async loadFileCollection(path, index = 0) { try { /* create file collection and get its data */ const fileCollection = new XC(path); await fileCollection.init(); this.apiFileCollections.push(fileCollection); const treeData = await fileCollection.read(); /* create a tree view and push it */ const data = new Tree(treeData.length && treeData[0].name === 'swagger.json' ? treeData[0].children : treeData); this.apiTvs.push(data); this.curApiCollectionPanel = this.apiFileCollections.length - 1; } catch (e) { console.log('Error in loadFileCollection:', e); throw e; } }, async loadHistoryFile() { // try { // this.fileHistory = new XcApis({path: config.electron.apiHistoryPath}); // this.historyList = await this.fileHistory.read(); // } catch (e) { // console.log('Error in loadFileHistory:', e); // throw e; // } }, async refreshFileCollection(index = 0) { try { const path = this.$store.getters['apiClientSwagger/GtrCurrentApiFilePaths'][index]; const fileCollection = new XC(path); await fileCollection.init(); this.apiFileCollections[index] = fileCollection; const data = new Tree(await fileCollection.read()); this.$set(this.apiTvs, index, data); } catch (e) { console.log('Error in loadFileCollection:', e); throw e; } }, async bookmarkApi(panelIndex) { this.bookmarkApiDialog = false; if (this.apiFileCollections.length) { delete this.currentApi.meta.history; const q = this.currentApi; const node = new TreeNode({ name: q.meta.path, swagger: {}, isLeaf: true, id: Date.now(), pid: 0, ...q, }); if (!this.apiTvs[panelIndex].children) { this.apiTvs[panelIndex].children = []; } this.apiTvs[panelIndex].addChildren(node); await this.savefileCollections(panelIndex); await this.refreshFileCollection(panelIndex); this.$toast .success( `API added to ${this.$store.getters['apiClientSwagger/GtrCurrentApiFilePaths'][panelIndex].fileName} collection successfully` ) .goAway(3000); } else { this.$toast.info('Create a new collection to bookmark the api.').goAway(3000); this.openNewCollection('/'); } }, async saveOrUpdateApi() { if (this.api.history) { this.bookmarkApiDialog = true; } else { const q = this.currentApi; const api = { ...q, meta: { ...this.api }, pid: 0, }; const activePanelIndex = this.curApiCollectionPanel; await this.apiFileCollections[activePanelIndex].updateApi({ api }); await this.refreshFileCollection(activePanelIndex); this.$toast .success( `API updated in ${this.$store.getters['apiClientSwagger/GtrCurrentApiFilePaths'][activePanelIndex].fileName} collection successfully` ) .goAway(3000); } }, }, beforeCreated() {}, destroy() {}, directives: {}, validate({ params }) { return true; }, head() { return {}; }, props: ['nodes'], }; </script> <style scoped> .apis-list { height: calc(100%); overflow-y: auto; } .apis-request { overflow-y: auto; } /deep/ > .req-inputs .v-toolbar__content { width: 100%; display: flex; padding: 2px; } /deep/ .req-tabs > .v-tabs-items { border-top: 1px solid #7f828b33; } .envs /deep/ .v-select__selections { color: var(--v-accent-base); } /*.envs /deep/ .v-select__selections input { display: none}*/ code { min-height: 200px; } </style> <!-- /** * @copyright Copyright (c) 2021, Xgene Cloud Ltd * * @author Naveen MR <oof1lab@gmail.com> * @author Pranav C Balan <pranavxc@gmail.com> * * @license GNU AGPL version 3 or any later version * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * */ -->