ui: refactor the UI with the nav menu (#297)

Co-authored-by: rick <LinuxSuRen@users.noreply.github.com>
This commit is contained in:
Rick 2023-11-29 18:26:14 +08:00 committed by GitHub
parent 40f27c5ba9
commit 02f23fea7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 644 additions and 574 deletions

View File

@ -8,6 +8,7 @@ describe('gRPC', () => {
it('Create Suite', () => {
cy.visit('/')
cy.get('.introjs-skipbutton').click()
cy.get('[test-id="testing-menu"]').click()
createTestSuite(store, 'gRPC', suiteName, sampleAPIAddress)
})

View File

@ -10,6 +10,7 @@ describe('Suite Manage', () => {
it('Create Suite', () => {
cy.visit('/')
cy.get('.introjs-skipbutton').click()
cy.get('[test-id="testing-menu"]').click()
cy.contains('span', 'Tool Box')
createTestSuite(store, 'HTTP', suiteName, sampleAPIAddress)

View File

@ -1,25 +1,20 @@
<script setup lang="ts">
import WelcomePage from './views/WelcomePage.vue'
import TestCase from './views/TestCase.vue'
import TestSuite from './views/TestSuite.vue'
import StoreManager from './views/StoreManager.vue'
import SecretManager from './views/SecretManager.vue'
import TemplateFunctions from './views/TemplateFunctions.vue'
import { reactive, ref, watch } from 'vue'
import { ElTree, ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { Edit, Share } from '@element-plus/icons-vue'
import type { Suite } from './types'
import {
Document,
Menu as IconMenu,
Location,
Share,
} from '@element-plus/icons-vue'
import { ref, watch } from 'vue'
import { API } from './views/net'
import { Cache } from './views/cache'
import { useI18n } from 'vue-i18n'
import ClientMonitor from 'skywalking-client-js'
import { name, version } from '../package'
import TestingPanel from './views/TestingPanel.vue'
import StoreManager from './views/StoreManager.vue'
import SecretManager from './views/SecretManager.vue'
import WelcomePage from './views/WelcomePage.vue'
import setAsDarkTheme from './theme'
const { t } = useI18n()
const asDarkMode = ref(Cache.GetPreference().darkTheme)
setAsDarkTheme(asDarkMode.value)
watch(asDarkMode, Cache.WatchDarkTheme)
@ -27,304 +22,6 @@ watch(asDarkMode, () => {
setAsDarkTheme(asDarkMode.value)
})
interface Tree {
id: string
label: string
parent: string
parentID: string
store: string
kind: string
children?: Tree[]
}
const testCaseName = ref('')
const testSuite = ref('')
const testSuiteKind = ref('')
const handleNodeClick = (data: Tree) => {
if (data.children) {
Cache.SetCurrentStore(data.store)
viewName.value = 'testsuite'
testSuite.value = data.label
testSuiteKind.value = data.kind
Cache.SetCurrentStore(data.store)
API.ListTestCase(data.label, data.store, (d) => {
if (d.items && d.items.length > 0) {
data.children = []
d.items.forEach((item: any) => {
data.children?.push({
id: data.label,
label: item.name,
kind: data.kind,
store: data.store,
parent: data.label,
parentID: data.id
} as Tree)
})
}
})
} else {
Cache.SetCurrentStore(data.store)
Cache.SetLastTestCaseLocation(data.parentID, data.id)
testCaseName.value = data.label
testSuite.value = data.parent
testSuiteKind.value = data.kind
viewName.value = 'testcase'
}
}
const data = ref([] as Tree[])
const treeRef = ref<InstanceType<typeof ElTree>>()
const currentNodekey = ref('')
function loadTestSuites(storeName: string) {
const requestOptions = {
method: 'POST',
headers: {
'X-Store-Name': storeName,
'X-Auth': API.getToken()
},
}
return async () => {
await fetch('/server.Runner/GetSuites', requestOptions)
.then((response) => response.json())
.then((d) => {
if (!d.data) {
return
}
Object.keys(d.data).map((k) => {
let suite = {
id: k,
label: k,
kind: d.data[k].kind,
store: storeName,
children: [] as Tree[]
} as Tree
d.data[k].data.forEach((item: any) => {
suite.children?.push({
id: k + item,
label: item,
store: storeName,
kind: suite.kind,
parent: k,
parentID: suite.id
} as Tree)
})
data.value.push(suite)
})
})
}
}
interface Store {
name: string,
description: string,
}
const loginDialogVisible = ref(false)
const stores = ref([] as Store[])
const storesLoading = ref(false)
function loadStores() {
storesLoading.value = true
const requestOptions = {
method: 'POST',
headers: {
'X-Auth': API.getToken()
}
}
fetch('/server.Runner/GetStores', requestOptions)
.then(API.DefaultResponseProcess)
.then(async (d) => {
stores.value = d.data
data.value = [] as Tree[]
Cache.SetStores(d.data)
for (const item of d.data) {
if (item.ready && !item.disabled) {
await loadTestSuites(item.name)()
}
}
if (data.value.length > 0) {
const key = Cache.GetLastTestCaseLocation()
let targetSuite = {} as Tree
let targetChild = {} as Tree
if (key.suite !== '' && key.testcase !== '') {
for (var i = 0; i < data.value.length; i++) {
const item = data.value[i]
if (item.id === key.suite && item.children) {
for (var j = 0; j < item.children.length; j++) {
const child = item.children[j]
if (child.id === key.testcase) {
targetSuite = item
targetChild = child
break
}
}
break
}
}
}
if (!targetChild.id || targetChild.id === '') {
targetSuite = data.value[0]
if (targetSuite.children && targetSuite.children.length > 0) {
targetChild = targetSuite.children[0]
}
}
viewName.value = 'testsuite'
currentNodekey.value = targetChild.id
treeRef.value!.setCurrentKey(targetChild.id)
treeRef.value!.setCheckedKeys([targetChild.id], false)
testSuite.value = targetSuite.label
Cache.SetCurrentStore(targetSuite.store )
testSuiteKind.value = targetChild.kind
} else {
viewName.value = ""
}
}).catch((e) => {
if(e.message === "Unauthenticated") {
loginDialogVisible.value = true
} else {
ElMessage.error('Oops, ' + e)
}
}).finally(() => {
storesLoading.value = false
})
}
loadStores()
const dialogVisible = ref(false)
const importDialogVisible = ref(false)
const suiteCreatingLoading = ref(false)
const suiteFormRef = ref<FormInstance>()
const testSuiteForm = reactive({
name: '',
api: '',
store: '',
kind: ''
})
const importSuiteFormRef = ref<FormInstance>()
const importSuiteForm = reactive({
url: '',
store: ''
})
function openTestSuiteCreateDialog() {
dialogVisible.value = true
}
function openTestSuiteImportDialog() {
importDialogVisible.value = true
}
const rules = reactive<FormRules<Suite>>({
name: [{ required: true, message: 'Name is required', trigger: 'blur' }],
store: [{ required: true, message: 'Location is required', trigger: 'blur' }]
})
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid: boolean, fields) => {
if (valid) {
suiteCreatingLoading.value = true
API.CreateTestSuite(testSuiteForm, (e) => {
suiteCreatingLoading.value = false
if (e.error !== "") {
ElMessage.error('Oops, ' + e.error)
} else {
loadStores()
dialogVisible.value = false
formEl.resetFields()
}
}, (e) => {
suiteCreatingLoading.value = false
ElMessage.error('Oops, ' + e)
})
}
})
}
const importSuiteFormRules = reactive<FormRules<Suite>>({
url: [
{ required: true, message: 'URL is required', trigger: 'blur' },
{ type: 'url', message: 'Should be a valid URL value', trigger: 'blur' }
],
store: [{ required: true, message: 'Location is required', trigger: 'blur' }]
})
const importSuiteFormSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid: boolean, fields) => {
if (valid) {
suiteCreatingLoading.value = true
API.ImportTestSuite(importSuiteForm, () => {
loadStores()
importDialogVisible.value = false
formEl.resetFields()
})
}
})
}
const filterText = ref('')
watch(filterText, (val) => {
treeRef.value!.filter(val)
})
const filterTestCases = (value: string, data: Tree) => {
if (!value) return true
return data.label.includes(value)
}
const viewName = ref('')
watch(viewName, (val) => {
ClientMonitor.setPerformance({
service: name,
serviceVersion: version,
pagePath: val,
useFmp: true,
enableSPA: true,
customTags: [{
key: 'theme', value: asDarkMode.value ? 'dark' : 'light'
}, {
key: 'store', value: Cache.GetCurrentStore().name
}]
});
})
const deviceAuthActive = ref(0)
const deviceAuthResponse = ref({
user_code: '',
verification_uri: '',
device_code: ''
})
const deviceAuthNext = () => {
if (deviceAuthActive.value++ > 2) {
return
}
if (deviceAuthActive.value === 1) {
fetch('/oauth2/getLocalCode')
.then(API.DefaultResponseProcess)
.then((d) => {
deviceAuthResponse.value = d
})
} else if (deviceAuthActive.value === 2) {
window.location.href = '/oauth2/getUserInfoFromLocalCode?device_code=' + deviceAuthResponse.value.device_code
}
}
const suiteKinds = [{
"name": "HTTP",
}, {
"name": "gRPC",
}, {
"name": "tRPC",
}]
const appVersion = ref('')
const appVersionLink = ref('https://github.com/LinuxSuRen/api-testing')
API.GetVersion((d) => {
@ -342,272 +39,64 @@ API.GetVersion((d) => {
appVersionLink.value = appVersionLink.value + '/releases/tag/' + version[0]
}
})
const panelName = ref('')
const sideWidth = ref("width: 300px")
const isCollapse = ref(false)
watch(isCollapse, (e) => {
console.log(e)
if (e) {
sideWidth.value = "width: 80px"
} else {
sideWidth.value = "width: 300px"
}
})
const handleSelect = (key: string) => {
panelName.value = key
}
</script>
<template>
<div class="common-layout" data-title="Welcome!" data-intro="Welcome to use api-testing! 👋">
<el-container style="height: 100%">
<el-header style="height: 30px;justify-content: flex-end;">
<el-button type="primary" :icon="Edit" @click="viewName = 'secret'" data-intro="Manage the secrets."/>
<el-button type="primary" :icon="Share" @click="viewName = 'store'" data-intro="Manage the store backends." />
<el-form-item label="Dark Mode" style="margin-left:20px;">
<el-switch type="primary" data-intro="Switch light and dark modes" v-model="asDarkMode" />
</el-form-item>
</el-header>
<el-container style="height: 100%">
<el-aside :style="sideWidth">
<el-radio-group v-model="isCollapse" style="margin-bottom: 20px">
<el-radio-button :label="false">+</el-radio-button>
<el-radio-button :label="true">-</el-radio-button>
</el-radio-group>
<el-menu
class="el-menu-vertical-demo"
default-active="welcome"
:collapse="isCollapse"
@select="handleSelect"
>
<el-menu-item index="welcome">
<el-icon><share /></el-icon>
<template #title>Welcome</template>
</el-menu-item>
<el-menu-item index="testing" test-id="testing-menu">
<el-icon><icon-menu /></el-icon>
<template #title>Testing</template>
</el-menu-item>
<el-menu-item index="secret">
<el-icon><document /></el-icon>
<template #title>Secrets</template>
</el-menu-item>
<el-menu-item index="store">
<el-icon><location /></el-icon>
<template #title>Stores</template>
</el-menu-item>
</el-menu>
</el-aside>
<el-main>
<el-container style="height: 100%">
<el-aside>
<el-button type="primary" @click="openTestSuiteCreateDialog"
data-intro="Click here to create a new test suite"
test-id="open-new-suite-dialog" :icon="Edit">{{ t('button.new') }}</el-button>
<el-button type="primary" @click="openTestSuiteImportDialog"
data-intro="Click here to import from Postman"
test-id="open-import-suite-dialog">{{ t('button.import') }}</el-button>
<el-input v-model="filterText" placeholder="Filter keyword" test-id="search" />
<el-main>
<TestingPanel v-if="panelName === 'testing'" />
<StoreManager v-else-if="panelName === 'store'" />
<SecretManager v-else-if="panelName === 'secret'" />
<WelcomePage v-else />
</el-main>
<el-tree
v-loading="storesLoading"
:data=data
highlight-current
:check-on-click-node="true"
:expand-on-click-node="false"
:current-node-key="currentNodekey"
ref="treeRef"
node-key="id"
:filter-node-method="filterTestCases"
@node-click="handleNodeClick"
data-intro="This is the test suite tree. You can click the test suite to edit it."
/>
</el-aside>
<el-main>
<WelcomePage
v-if="viewName === ''"
/>
<TestCase
v-else-if="viewName === 'testcase'"
:suite="testSuite"
:kindName="testSuiteKind"
:name="testCaseName"
@updated="loadStores"
data-intro="This is the test case editor. You can edit the test case here."
/>
<TestSuite
v-else-if="viewName === 'testsuite'"
:name="testSuite"
@updated="loadStores"
data-intro="This is the test suite editor. You can edit the test suite here."
/>
<StoreManager
v-else-if="viewName === 'store'"
/>
<SecretManager
v-else-if="viewName === 'secret'"
/>
</el-main>
</el-container>
</el-main>
<div style="position: absolute; bottom: 0px; right: 10px;">
<a :href=appVersionLink target="_blank" rel="noopener">{{appVersion}}</a>
</div>
<TemplateFunctions/>
</el-container>
</div>
<el-dialog v-model="dialogVisible" :title="t('title.createTestSuite')" width="30%" draggable>
<template #footer>
<span class="dialog-footer">
<el-form
:rules="rules"
:model="testSuiteForm"
ref="suiteFormRef"
status-icon label-width="120px">
<el-form-item :label="t('field.storageLocation')" prop="store">
<el-select v-model="testSuiteForm.store" class="m-2"
test-id="suite-form-store"
filterable=true
default-first-option=true
placeholder="Storage Location" size="middle">
<el-option
v-for="item in stores"
:key="item.name"
:label="item.name"
:value="item.name"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('field.suiteKind')" prop="kind">
<el-select v-model="testSuiteForm.kind" class="m-2"
filterable=true
test-id="suite-form-kind"
default-first-option=true
size="middle">
<el-option
v-for="item in suiteKinds"
:key="item.name"
:label="item.name"
:value="item.name"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('field.name')" prop="name">
<el-input v-model="testSuiteForm.name" test-id="suite-form-name" />
</el-form-item>
<el-form-item label="API" prop="api">
<el-input v-model="testSuiteForm.api" placeholder="http://foo" test-id="suite-form-api" />
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="submitForm(suiteFormRef)"
:loading="suiteCreatingLoading"
test-id="suite-form-submit"
>{{ t('button.submit') }}</el-button
>
</el-form-item>
</el-form>
</span>
</template>
</el-dialog>
<el-dialog v-model="importDialogVisible" title="Import Test Suite" width="30%" draggable>
<span>Supported source URL: Postman collection share link</span>
<template #footer>
<span class="dialog-footer">
<el-form
:rules="importSuiteFormRules"
:model="importSuiteForm"
ref="importSuiteFormRef"
status-icon label-width="120px">
<el-form-item label="Location" prop="store">
<el-select v-model="importSuiteForm.store" class="m-2"
test-id="suite-import-form-store"
filterable=true
default-first-option=true
placeholder="Storage Location" size="middle">
<el-option
v-for="item in stores"
:key="item.name"
:label="item.name"
:value="item.name"
/>
</el-select>
</el-form-item>
<el-form-item label="URL" prop="url">
<el-input v-model="importSuiteForm.url" test-id="suite-import-form-api" placeholder="https://api.postman.com/collections/xxx" />
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="importSuiteFormSubmit(importSuiteFormRef)"
test-id="suite-import-submit"
>{{ t('button.import') }}</el-button
>
</el-form-item>
</el-form>
</span>
</template>
</el-dialog>
<el-dialog
v-model="loginDialogVisible"
title="You need to login first."
width="30%"
>
<el-collapse accordion="true">
<el-collapse-item title="Server in cloud" name="1">
<a href="/oauth2/token" target="_blank">
<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true" class="octicon octicon-mark-github v-align-middle color-fg-default">
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
</svg>
</a>
</el-collapse-item>
<el-collapse-item title="Server in local" name="2">
<el-steps :active="deviceAuthActive" finish-status="success">
<el-step title="Request Device Code" />
<el-step title="Input Code"/>
<el-step title="Finished" />
</el-steps>
<div v-if="deviceAuthActive===1">
Open <a :href="deviceAuthResponse.verification_uri" target="_blank">this link</a>, and type the code: <span>{{ deviceAuthResponse.user_code }}. Then click the next step button.</span>
</div>
<el-button style="margin-top: 12px" @click="deviceAuthNext">Next step</el-button>
</el-collapse-item>
</el-collapse>
</el-dialog>
</el-container>
</template>
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
.common-layout {
height: 100%;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
.demo-tabs > .el-tabs__content {
padding: 32px;
color: #6b778c;
font-size: 32px;
font-weight: 600;
}
</style>

View File

@ -0,0 +1,549 @@
<script setup lang="ts">
import WelcomePage from './WelcomePage.vue'
import TestCase from './TestCase.vue'
import TestSuite from './TestSuite.vue'
import TemplateFunctions from './TemplateFunctions.vue'
import { reactive, ref, watch } from 'vue'
import { ElTree, ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { Edit } from '@element-plus/icons-vue'
import type { Suite } from './types'
import { API } from './net'
import { Cache } from './cache'
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
interface Tree {
id: string
label: string
parent: string
parentID: string
store: string
kind: string
children?: Tree[]
}
const testCaseName = ref('')
const testSuite = ref('')
const testSuiteKind = ref('')
const handleNodeClick = (data: Tree) => {
if (data.children) {
Cache.SetCurrentStore(data.store)
viewName.value = 'testsuite'
testSuite.value = data.label
testSuiteKind.value = data.kind
Cache.SetCurrentStore(data.store)
API.ListTestCase(data.label, data.store, (d) => {
if (d.items && d.items.length > 0) {
data.children = []
d.items.forEach((item: any) => {
data.children?.push({
id: data.label,
label: item.name,
kind: data.kind,
store: data.store,
parent: data.label,
parentID: data.id
} as Tree)
})
}
})
} else {
Cache.SetCurrentStore(data.store)
Cache.SetLastTestCaseLocation(data.parentID, data.id)
testCaseName.value = data.label
testSuite.value = data.parent
testSuiteKind.value = data.kind
viewName.value = 'testcase'
}
}
const data = ref([] as Tree[])
const treeRef = ref<InstanceType<typeof ElTree>>()
const currentNodekey = ref('')
function loadTestSuites(storeName: string) {
const requestOptions = {
method: 'POST',
headers: {
'X-Store-Name': storeName,
'X-Auth': API.getToken()
},
}
return async () => {
await fetch('/server.Runner/GetSuites', requestOptions)
.then((response) => response.json())
.then((d) => {
if (!d.data) {
return
}
Object.keys(d.data).map((k) => {
let suite = {
id: k,
label: k,
kind: d.data[k].kind,
store: storeName,
children: [] as Tree[]
} as Tree
d.data[k].data.forEach((item: any) => {
suite.children?.push({
id: k + item,
label: item,
store: storeName,
kind: suite.kind,
parent: k,
parentID: suite.id
} as Tree)
})
data.value.push(suite)
})
})
}
}
interface Store {
name: string,
description: string,
}
const loginDialogVisible = ref(false)
const stores = ref([] as Store[])
const storesLoading = ref(false)
function loadStores() {
storesLoading.value = true
const requestOptions = {
method: 'POST',
headers: {
'X-Auth': API.getToken()
}
}
fetch('/server.Runner/GetStores', requestOptions)
.then(API.DefaultResponseProcess)
.then(async (d) => {
stores.value = d.data
data.value = [] as Tree[]
Cache.SetStores(d.data)
for (const item of d.data) {
if (item.ready && !item.disabled) {
await loadTestSuites(item.name)()
}
}
if (data.value.length > 0) {
const key = Cache.GetLastTestCaseLocation()
let targetSuite = {} as Tree
let targetChild = {} as Tree
if (key.suite !== '' && key.testcase !== '') {
for (var i = 0; i < data.value.length; i++) {
const item = data.value[i]
if (item.id === key.suite && item.children) {
for (var j = 0; j < item.children.length; j++) {
const child = item.children[j]
if (child.id === key.testcase) {
targetSuite = item
targetChild = child
break
}
}
break
}
}
}
if (!targetChild.id || targetChild.id === '') {
targetSuite = data.value[0]
if (targetSuite.children && targetSuite.children.length > 0) {
targetChild = targetSuite.children[0]
}
}
viewName.value = 'testsuite'
currentNodekey.value = targetChild.id
treeRef.value!.setCurrentKey(targetChild.id)
treeRef.value!.setCheckedKeys([targetChild.id], false)
testSuite.value = targetSuite.label
Cache.SetCurrentStore(targetSuite.store )
testSuiteKind.value = targetChild.kind
} else {
viewName.value = ""
}
}).catch((e) => {
if(e.message === "Unauthenticated") {
loginDialogVisible.value = true
} else {
ElMessage.error('Oops, ' + e)
}
}).finally(() => {
storesLoading.value = false
})
}
loadStores()
const dialogVisible = ref(false)
const importDialogVisible = ref(false)
const suiteCreatingLoading = ref(false)
const suiteFormRef = ref<FormInstance>()
const testSuiteForm = reactive({
name: '',
api: '',
store: '',
kind: ''
})
const importSuiteFormRef = ref<FormInstance>()
const importSuiteForm = reactive({
url: '',
store: ''
})
function openTestSuiteCreateDialog() {
dialogVisible.value = true
}
function openTestSuiteImportDialog() {
importDialogVisible.value = true
}
const rules = reactive<FormRules<Suite>>({
name: [{ required: true, message: 'Name is required', trigger: 'blur' }],
store: [{ required: true, message: 'Location is required', trigger: 'blur' }]
})
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid: boolean) => {
if (valid) {
suiteCreatingLoading.value = true
API.CreateTestSuite(testSuiteForm, (e) => {
suiteCreatingLoading.value = false
if (e.error !== "") {
ElMessage.error('Oops, ' + e.error)
} else {
loadStores()
dialogVisible.value = false
formEl.resetFields()
}
}, (e) => {
suiteCreatingLoading.value = false
ElMessage.error('Oops, ' + e)
})
}
})
}
const importSuiteFormRules = reactive<FormRules<Suite>>({
url: [
{ required: true, message: 'URL is required', trigger: 'blur' },
{ type: 'url', message: 'Should be a valid URL value', trigger: 'blur' }
],
store: [{ required: true, message: 'Location is required', trigger: 'blur' }]
})
const importSuiteFormSubmit = async (formEl: FormInstance | undefined) => {
if (!formEl) return
await formEl.validate((valid: boolean) => {
if (valid) {
suiteCreatingLoading.value = true
API.ImportTestSuite(importSuiteForm, () => {
loadStores()
importDialogVisible.value = false
formEl.resetFields()
})
}
})
}
const filterText = ref('')
watch(filterText, (val) => {
treeRef.value!.filter(val)
})
const filterTestCases = (value: string, data: Tree) => {
if (!value) return true
return data.label.includes(value)
}
const viewName = ref('')
const deviceAuthActive = ref(0)
const deviceAuthResponse = ref({
user_code: '',
verification_uri: '',
device_code: ''
})
const deviceAuthNext = () => {
if (deviceAuthActive.value++ > 2) {
return
}
if (deviceAuthActive.value === 1) {
fetch('/oauth2/getLocalCode')
.then(API.DefaultResponseProcess)
.then((d) => {
deviceAuthResponse.value = d
})
} else if (deviceAuthActive.value === 2) {
window.location.href = '/oauth2/getUserInfoFromLocalCode?device_code=' + deviceAuthResponse.value.device_code
}
}
const suiteKinds = [{
"name": "HTTP",
}, {
"name": "gRPC",
}, {
"name": "tRPC",
}]
</script>
<template>
<div class="common-layout" data-title="Welcome!" data-intro="Welcome to use api-testing! 👋">
<el-container style="height: 100%">
<el-main>
<el-container style="height: 100%">
<el-aside>
<el-button type="primary" @click="openTestSuiteCreateDialog"
data-intro="Click here to create a new test suite"
test-id="open-new-suite-dialog" :icon="Edit">{{ t('button.new') }}</el-button>
<el-button type="primary" @click="openTestSuiteImportDialog"
data-intro="Click here to import from Postman"
test-id="open-import-suite-dialog">{{ t('button.import') }}</el-button>
<el-input v-model="filterText" placeholder="Filter keyword" test-id="search" />
<el-tree
v-loading="storesLoading"
:data=data
highlight-current
:check-on-click-node="true"
:expand-on-click-node="false"
:current-node-key="currentNodekey"
ref="treeRef"
node-key="id"
:filter-node-method="filterTestCases"
@node-click="handleNodeClick"
data-intro="This is the test suite tree. You can click the test suite to edit it."
/>
</el-aside>
<el-main>
<TestCase
v-if="viewName === 'testcase'"
:suite="testSuite"
:kindName="testSuiteKind"
:name="testCaseName"
@updated="loadStores"
data-intro="This is the test case editor. You can edit the test case here."
/>
<TestSuite
v-else-if="viewName === 'testsuite'"
:name="testSuite"
@updated="loadStores"
data-intro="This is the test suite editor. You can edit the test suite here."
/>
</el-main>
</el-container>
</el-main>
<TemplateFunctions/>
</el-container>
</div>
<el-dialog v-model="dialogVisible" :title="t('title.createTestSuite')" width="30%" draggable>
<template #footer>
<span class="dialog-footer">
<el-form
:rules="rules"
:model="testSuiteForm"
ref="suiteFormRef"
status-icon label-width="120px">
<el-form-item :label="t('field.storageLocation')" prop="store">
<el-select v-model="testSuiteForm.store" class="m-2"
test-id="suite-form-store"
filterable=true
default-first-option=true
placeholder="Storage Location" size="middle">
<el-option
v-for="item in stores"
:key="item.name"
:label="item.name"
:value="item.name"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('field.suiteKind')" prop="kind">
<el-select v-model="testSuiteForm.kind" class="m-2"
filterable=true
test-id="suite-form-kind"
default-first-option=true
size="middle">
<el-option
v-for="item in suiteKinds"
:key="item.name"
:label="item.name"
:value="item.name"
/>
</el-select>
</el-form-item>
<el-form-item :label="t('field.name')" prop="name">
<el-input v-model="testSuiteForm.name" test-id="suite-form-name" />
</el-form-item>
<el-form-item label="API" prop="api">
<el-input v-model="testSuiteForm.api" placeholder="http://foo" test-id="suite-form-api" />
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="submitForm(suiteFormRef)"
:loading="suiteCreatingLoading"
test-id="suite-form-submit"
>{{ t('button.submit') }}</el-button
>
</el-form-item>
</el-form>
</span>
</template>
</el-dialog>
<el-dialog v-model="importDialogVisible" title="Import Test Suite" width="30%" draggable>
<span>Supported source URL: Postman collection share link</span>
<template #footer>
<span class="dialog-footer">
<el-form
:rules="importSuiteFormRules"
:model="importSuiteForm"
ref="importSuiteFormRef"
status-icon label-width="120px">
<el-form-item label="Location" prop="store">
<el-select v-model="importSuiteForm.store" class="m-2"
test-id="suite-import-form-store"
filterable=true
default-first-option=true
placeholder="Storage Location" size="middle">
<el-option
v-for="item in stores"
:key="item.name"
:label="item.name"
:value="item.name"
/>
</el-select>
</el-form-item>
<el-form-item label="URL" prop="url">
<el-input v-model="importSuiteForm.url" test-id="suite-import-form-api" placeholder="https://api.postman.com/collections/xxx" />
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="importSuiteFormSubmit(importSuiteFormRef)"
test-id="suite-import-submit"
>{{ t('button.import') }}</el-button
>
</el-form-item>
</el-form>
</span>
</template>
</el-dialog>
<el-dialog
v-model="loginDialogVisible"
title="You need to login first."
width="30%"
>
<el-collapse accordion="true">
<el-collapse-item title="Server in cloud" name="1">
<a href="/oauth2/token" target="_blank">
<svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true" class="octicon octicon-mark-github v-align-middle color-fg-default">
<path d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
</svg>
</a>
</el-collapse-item>
<el-collapse-item title="Server in local" name="2">
<el-steps :active="deviceAuthActive" finish-status="success">
<el-step title="Request Device Code" />
<el-step title="Input Code"/>
<el-step title="Finished" />
</el-steps>
<div v-if="deviceAuthActive===1">
Open <a :href="deviceAuthResponse.verification_uri" target="_blank">this link</a>, and type the code: <span>{{ deviceAuthResponse.user_code }}. Then click the next step button.</span>
</div>
<el-button style="margin-top: 12px" @click="deviceAuthNext">Next step</el-button>
</el-collapse-item>
</el-collapse>
</el-dialog>
</template>
<style scoped>
header {
line-height: 1.5;
max-height: 100vh;
}
.common-layout {
height: 100%;
}
.logo {
display: block;
margin: 0 auto 2rem;
}
nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
}
nav a.router-link-exact-active {
color: var(--color-text);
}
nav a.router-link-exact-active:hover {
background-color: transparent;
}
nav a {
display: inline-block;
padding: 0 1rem;
border-left: 1px solid var(--color-border);
}
nav a:first-of-type {
border: 0;
}
@media (min-width: 1024px) {
header {
display: flex;
place-items: center;
padding-right: calc(var(--section-gap) / 2);
}
.logo {
margin: 0 2rem 0 0;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
nav {
text-align: left;
margin-left: -1rem;
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
.demo-tabs > .el-tabs__content {
padding: 32px;
color: #6b778c;
font-size: 32px;
font-weight: 600;
}
</style>

View File

@ -251,13 +251,17 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUu
gioui.org v0.0.0-20210308172011-57750fc8a0a6 h1:K72hopUosKG3ntOPNG4OzzbuhxGuVf06fa2la1/H/Ho=
git.sr.ht/~sbinet/gg v0.3.1 h1:LNhjNn8DerC8f9DHLz6lS0YYul/b602DUxDgGkd/Aik=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
github.com/ClickHouse/clickhouse-go v1.5.4 h1:cKjXeYLNWVJIx2J1K6H2CqyRmfwVJVY1OV1coaaFcI0=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c h1:RGWPOewvKIROun94nF7v2cua9qP+thov/7M50KEoeSU=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/agiledragon/gomonkey v2.0.2+incompatible/go.mod h1:2NGfXu1a80LLr2cmWXGBDaHEjb1idR6+FVlX5T3D9hw=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9 h1:7kQgkwGRoLzC9K0oyXdJo7nve/bynv/KwUsxbiTlzAM=
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19 h1:iXUgAaqDcIUGbRoy2TdeofRG/j1zpGRSEmNK05T+bi8=
@ -280,13 +284,16 @@ github.com/armon/go-metrics v0.3.10 h1:FR+drcQStOe+32sYyJYyZ7FIdgoGGBnwLl+flodp8
github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyXcs=
github.com/bwesterb/go-ristretto v1.2.3 h1:1w53tCkGhCQ5djbat3+MH0BAQ5Kfgbt56UZQ/JMzngw=
github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4=
github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/readline v1.5.0 h1:lSwwFrbNviGePhkewF1az4oLmcwqCZijQ2/Wi3BGHAI=
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
@ -310,10 +317,12 @@ github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byA
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c=
github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ=
@ -329,16 +338,19 @@ github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBj
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 h1:6zl3BbBhdnMkpSj2YY30qV3gDcVBGtFgVsV3+/i+mKQ=
github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-pdf/fpdf v0.6.0 h1:MlgtGIfsdMEEQJr2le6b/HNr1ZlQwxyWr77r2aj2U/8=
github.com/go-playground/form/v4 v4.2.0/go.mod h1:q1a2BY+AQUUzhl6xA/6hBetay6dEIhMHjgvJiGo6K7U=
github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/cel-go v0.12.5 h1:DmzaiSgoaqGCjtpPQWl26/gND+yRpim56H1jCVev6d8=
github.com/google/cel-go v0.12.5/go.mod h1:Jk7ljRzLBhkmiAwBoUxB1sZSCVBAzkqPF25olK/iRDw=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
@ -355,9 +367,13 @@ github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5i
github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc/grpc-go v1.55.0 h1:8UgTz8i19QobJ8MwLklNLqXDoMHRBGS0ZI7h2OrVqYc=
github.com/grpc/grpc-go v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8=
@ -377,6 +393,8 @@ github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR3
github.com/hashicorp/serf v0.9.7 h1:hkdgbqizGQHuU5IPqYM1JdSMV8nKfpuOnZYXssk9muY=
github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
@ -386,6 +404,7 @@ github.com/jhump/goprotoc v0.5.0 h1:Y1UgUX+txUznfqcGdDef8ZOVlyQvnV0pKWZH08RmZuo=
github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ=
github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
@ -409,6 +428,7 @@ github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYt
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
@ -416,14 +436,17 @@ github.com/mattn/go-sqlite3 v1.14.14 h1:qZgc/Rwetq+MtyE18WhzjokPD93dNqLGNT3QJuLv
github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8 h1:AMFGa4R4MiIpspGNG7Z948v4n35fFGB3RR3G/ry4FWs=
github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3 h1:+n/aFZefKZp7spd8DFdX7uMikMLXX4oubIzJF4kv/wI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ=
github.com/mmcloughlin/avo v0.5.0 h1:nAco9/aI9Lg2kiuROBY6BhCI/z0t5jEvJfjWbL8qXLU=
github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae h1:O4SWKdcHVCvYqyDV+9CJA1fcDN2L11Bule0iFy3YlAI=
github.com/moby/term v0.0.0-20220808134915-39b0c02b01ae/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/nats-io/nats-server/v2 v2.9.15/go.mod h1:QlCTy115fqpx4KSOPFIxSV7DdI6OxtZsGOL1JLdeRlE=
github.com/nats-io/nats.go v1.25.0/go.mod h1:D2WALIhz7V8M0pH8Scx8JZXlg6Oqz5VG+nQkK8nJdvg=
github.com/onsi/ginkgo v1.13.0 h1:M76yO2HkZASFjXL0HSoZJ1AYEmQxNJmY41Jx1zNUq1Y=
@ -431,6 +454,7 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/phpdave11/gofpdf v1.4.2 h1:KPKiIbfwbvC/wOncwhrpRdXVj2CZTCFlw4wnoyjtHfQ=
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
@ -448,20 +472,24 @@ github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5A
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA=
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/xhit/go-str2duration v1.2.0 h1:BcV5u025cITWxEQKGWr1URRzrcXtu7uk8+luz3Yuhwc=
github.com/xhit/go-str2duration v1.2.0/go.mod h1:3cPSlfZlUHVlneIVfePFWcJZsuwf+P1v2SRTV4cUmp4=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489 h1:1JFLBqwIgdyHN1ZtgjTBwO+blA6gVOmZurpiMEsETKo=
go.etcd.io/etcd/api/v3 v3.5.5/go.mod h1:KFtNaxGDw4Yx/BA4iPPwevUTAuqcsPxzyX8PHydchN8=
@ -539,7 +567,9 @@ google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQf
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
honnef.co/go/tools v0.1.3 h1:qTakTkI6ni6LFD5sBwwsdSO+AQqbSIxOauHTTQKZ/7o=
k8s.io/apiserver v0.26.0 h1:q+LqIK5EZwdznGZb8bq0+a+vCqdeEEe4Ux3zsOjbc4o=
k8s.io/apiserver v0.26.0/go.mod h1:aWhlLD+mU+xRo+zhkvP/gFNbShI4wBDHS33o0+JGI84=