Skip to content

Commit 4ac2eaa

Browse files
authored
feat: add ui extension point (#801)
* add ui extension point * ui extension point patch * generate grpc code * fix the missing implement methods * move vault ui page into extension * support load menus from all stores * avoid extension taking the system menu name * support loading mock config from local file * add more unit tests for mock service * improve the mock service ui page --------- Co-authored-by: rick <[email protected]>
1 parent 0b03f7e commit 4ac2eaa

26 files changed

+3626
-2324
lines changed

.editorconfig

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ root = true
77
[*]
88
end_of_line = lf
99
insert_final_newline = true
10+
trim_trailing_whitespace = true
1011
charset = utf-8
1112

1213
# 4 space indentation
13-
[*.{py,proto,go,js,ts,json,vue}]
14+
[*.{py,proto,js,ts,json,vue}]
1415
indent_style = space
1516
indent_size = 4
17+
18+
[*.go]
19+
indent_style = tab

cmd/mock.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ type mockOption struct {
3131
port int
3232
prefix string
3333
metrics bool
34+
tls bool
35+
tlsCert string
36+
tlsKey string
3437
}
3538

3639
func createMockCmd() (c *cobra.Command) {
@@ -47,12 +50,18 @@ func createMockCmd() (c *cobra.Command) {
4750
flags.IntVarP(&opt.port, "port", "", 6060, "The mock server port")
4851
flags.StringVarP(&opt.prefix, "prefix", "", "/mock", "The mock server API prefix")
4952
flags.BoolVarP(&opt.metrics, "metrics", "m", true, "Enable request metrics collection")
53+
flags.BoolVarP(&opt.tls, "tls", "", false, "Enable TLS mode. Set to true to enable TLS. Alow SAN certificates")
54+
flags.StringVarP(&opt.tlsCert, "cert-file", "", "", "The path to the certificate file, Alow SAN certificates")
55+
flags.StringVarP(&opt.tlsKey, "key-file", "", "", "The path to the key file, Alow SAN certificates")
5056
return
5157
}
5258

5359
func (o *mockOption) runE(c *cobra.Command, args []string) (err error) {
5460
reader := mock.NewLocalFileReader(args[0])
5561
server := mock.NewInMemoryServer(c.Context(), o.port)
62+
if o.tls {
63+
server.WithTLS(o.tlsCert, o.tlsKey)
64+
}
5665
if o.metrics {
5766
server.EnableMetrics()
5867
}

cmd/server.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
297297
server.RegisterMockServer(s, mockServerController)
298298
server.RegisterDataServerServer(s, remoteServer.(server.DataServerServer))
299299
server.RegisterThemeExtensionServer(s, remoteServer.(server.ThemeExtensionServer))
300+
server.RegisterUIExtensionServer(s, remoteServer.(server.UIExtensionServer))
300301
serverLogger.Info("gRPC server listening at", "addr", lis.Addr())
301302
s.Serve(lis)
302303
}()
@@ -345,15 +346,19 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) {
345346
server.RegisterRunnerHandlerFromEndpoint(ctx, mux, gRPCServerAddr, opts),
346347
server.RegisterMockHandlerFromEndpoint(ctx, mux, gRPCServerAddr, opts),
347348
server.RegisterThemeExtensionHandlerFromEndpoint(ctx, mux, gRPCServerAddr, opts),
348-
server.RegisterDataServerHandlerFromEndpoint(ctx, mux, gRPCServerAddr, opts))
349+
server.RegisterDataServerHandlerFromEndpoint(ctx, mux, gRPCServerAddr, opts),
350+
server.RegisterUIExtensionHandlerFromEndpoint(ctx, mux, gRPCServerAddr, opts),
351+
)
349352
} else {
350353
dialOption := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials()),
351354
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(math.MaxInt))}
352355
err = errors.Join(
353356
server.RegisterRunnerHandlerFromEndpoint(ctx, mux, gRPCServerAddr, dialOption),
354357
server.RegisterMockHandlerFromEndpoint(ctx, mux, gRPCServerAddr, dialOption),
355358
server.RegisterThemeExtensionHandlerFromEndpoint(ctx, mux, gRPCServerAddr, dialOption),
356-
server.RegisterDataServerHandlerFromEndpoint(ctx, mux, gRPCServerAddr, dialOption))
359+
server.RegisterDataServerHandlerFromEndpoint(ctx, mux, gRPCServerAddr, dialOption),
360+
server.RegisterUIExtensionHandlerFromEndpoint(ctx, mux, gRPCServerAddr, dialOption),
361+
)
357362
}
358363

359364
if err == nil {

console/atest-ui/src/App.vue

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<script setup lang="ts">
22
import {
3-
Document,
43
Menu as IconMenu,
54
Histogram,
65
Location,
@@ -16,10 +15,10 @@ import TestingPanel from './views/TestingPanel.vue'
1615
import TestingHistoryPanel from './views/TestingHistoryPanel.vue'
1716
import MockManager from './views/MockManager.vue'
1817
import StoreManager from './views/StoreManager.vue'
19-
import SecretManager from './views/SecretManager.vue'
2018
import WelcomePage from './views/WelcomePage.vue'
2119
import DataManager from './views/DataManager.vue'
2220
import MagicKey from './components/MagicKey.vue'
21+
import Extension from './views/Extension.vue'
2322
import { useI18n } from 'vue-i18n'
2423
import ElementPlus from 'element-plus';
2524
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
@@ -109,6 +108,19 @@ const theme = ref(getTheme())
109108
watch(theme, (e) => {
110109
setTheme(e)
111110
})
111+
112+
interface Menu {
113+
name: string
114+
icon: string
115+
index: string
116+
}
117+
118+
const extensionMenus = ref([] as Menu[]);
119+
API.GetMenus((menus) => {
120+
if (menus.data && menus.data.length > 0) {
121+
extensionMenus.value = menus.data;
122+
}
123+
});
112124
</script>
113125

114126
<template>
@@ -144,14 +156,16 @@ watch(theme, (e) => {
144156
<el-icon><DataAnalysis /></el-icon>
145157
<template #title>{{ t('title.data' )}}</template>
146158
</el-menu-item>
147-
<el-menu-item index="secret">
148-
<el-icon><document /></el-icon>
149-
<template #title>{{ t('title.secrets') }}</template>
150-
</el-menu-item>
151159
<el-menu-item index="store">
152160
<el-icon><location /></el-icon>
153161
<template #title>{{ t('title.stores') }}</template>
154162
</el-menu-item>
163+
<span v-for="menu in extensionMenus" :key="menu.index" :index="menu.index">
164+
<el-menu-item :index="menu.index">
165+
<el-icon><IconMenu /></el-icon>
166+
<template #title>{{ menu.name }}</template>
167+
</el-menu-item>
168+
</span>
155169
</el-menu>
156170
</el-aside>
157171

@@ -166,8 +180,11 @@ watch(theme, (e) => {
166180
<DataManager v-else-if="panelName === 'data'" />
167181
<MockManager v-else-if="panelName === 'mock'" />
168182
<StoreManager v-else-if="panelName === 'store'" />
169-
<SecretManager v-else-if="panelName === 'secret'" />
170-
<WelcomePage v-else />
183+
<WelcomePage v-else-if="panelName === 'welcome' || panelName === ''" />
184+
185+
<span v-for="menu in extensionMenus" :key="menu.index" :index="menu.index">
186+
<Extension v-if="panelName === menu.index" :name="menu.name" />
187+
</span>
171188
</el-main>
172189

173190
<div style="position: absolute; bottom: 0px; right: 10px;">
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script setup lang="ts">
2+
import { API } from './net';
3+
4+
interface Props {
5+
name: string
6+
}
7+
const props = defineProps<Props>()
8+
9+
const loadPlugin = async (): Promise<void> => {
10+
try {
11+
API.GetPageOfCSS(props.name, (d) => {
12+
const style = document.createElement('style');
13+
style.type = 'text/css';
14+
style.textContent = d.message;
15+
document.head.appendChild(style);
16+
});
17+
18+
API.GetPageOfJS(props.name, (d) => {
19+
const script = document.createElement('script');
20+
script.type = 'text/javascript';
21+
script.textContent = d.message;
22+
document.head.appendChild(script);
23+
24+
const plugin = window.ATestPlugin;
25+
26+
if (plugin && plugin.mount) {
27+
console.log('extension load success');
28+
const container = document.getElementById("plugin-container");
29+
plugin.mount(container);
30+
}
31+
});
32+
} catch (error) {
33+
console.log(`extension load error: ${(error as Error).message}`)
34+
} finally {
35+
console.log('extension load finally');
36+
}
37+
};
38+
try {
39+
loadPlugin();
40+
} catch (error) {
41+
console.error('extension load error:', error);
42+
}
43+
</script>
44+
45+
<template>
46+
<div id="plugin-container">
47+
{{ props.name }}
48+
</div>
49+
</template>

console/atest-ui/src/views/MockManager.vue

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import { ref, watch } from 'vue';
33
import { Codemirror } from 'vue-codemirror';
4+
import { ElMessage } from 'element-plus'
45
import yaml from 'js-yaml';
56
import { jsonSchema } from "codemirror-json-schema";
67
import { NewTemplateLangComplete, NewHeaderLangComplete } from './languageComplete'
@@ -34,6 +35,10 @@ interface MockConfig {
3435
ConfigAsJSON: string
3536
Prefix: string
3637
Port: number
38+
storeKind: string
39+
storeLocalFile?: string
40+
storeURL?: string
41+
storeRemote?: string
3742
}
3843
3944
const tabActive = ref('yaml')
@@ -64,10 +69,18 @@ function jsonToYaml(jsonData: object | string): string {
6469
}
6570
6671
const link = ref('')
67-
API.GetMockConfig((d) => {
68-
mockConfig.value = d
69-
link.value = `http://${window.location.hostname}:${d.Port}${d.Prefix}/api.json`
70-
})
72+
const loadConfig = () => {
73+
API.GetMockConfig((d) => {
74+
ElMessage({
75+
showClose: true,
76+
message: 'Config loaded!',
77+
type: 'success'
78+
});
79+
mockConfig.value = d
80+
link.value = `http://${window.location.hostname}:${d.Port}${d.Prefix}/api.json`
81+
})
82+
}
83+
loadConfig()
7184
const prefixChanged = (p: string) => {
7285
mockConfig.value.Prefix = p
7386
}
@@ -96,13 +109,21 @@ items:
96109
<template>
97110
<div>
98111
<el-button type="primary" @click="insertSample">{{t('button.insertSample')}}</el-button>
99-
<el-button type="warning" @click="API.ReloadMockServer(mockConfig)">{{t('button.reload')}}</el-button>
112+
<el-button type="warning" @click="API.ReloadMockServer(mockConfig).then(() => loadConfig())">{{t('button.reload')}}</el-button>
100113
<el-divider direction="vertical" />
101114
<el-link target="_blank" :href="link">{{ link }}</el-link> <!-- Noncompliant -->
102115
</div>
103116
<div class="config">
104117
API Prefix:<EditButton :value="mockConfig.Prefix" @changed="prefixChanged"/>
105118
Port:<EditButton :value="mockConfig.Port" @changed="portChanged"/>
119+
Store:
120+
<el-select v-model="mockConfig.storeKind" placeholder="Select Store Kind"
121+
class="m-2 select"
122+
size="default">
123+
<el-option label="Memory" value="memory"></el-option>
124+
<el-option label="Local File" value="localFile"></el-option>
125+
</el-select>
126+
<el-input v-model="mockConfig.storeLocalFile" placeholder="Local File Path" v-if="mockConfig.storeKind === 'localFile'"></el-input>
106127
</div>
107128
<el-splitter layout="vertical" style="height: calc(100vh - 100px);">
108129
<el-splitter-panel size="70%">
@@ -118,12 +139,12 @@ items:
118139
</el-tabs>
119140
</el-splitter-panel>
120141
<el-splitter-panel size="30%">
121-
<el-card class="log-output" shadow="hover">
142+
<el-card shadow="hover">
122143
<template #header>
123144
<span>{{ t('title.logs') }}</span>
124145
</template>
125-
<el-scrollbar ref="logScrollbar">
126-
<pre style="white-space: pre-wrap; word-break: break-all;">{{ logOutput }}</pre>
146+
<el-scrollbar>
147+
<pre style="white-space: pre-wrap; word-break: break-all;">{{logOutput}}</pre>
127148
</el-scrollbar>
128149
</el-card>
129150
</el-splitter-panel>
@@ -137,8 +158,10 @@ items:
137158
align-items: center;
138159
gap: 8px;
139160
}
140-
.log-output {
141-
height: 100%;
142-
overflow: auto;
161+
.select {
162+
width: 150px !important;
163+
}
164+
.el-input {
165+
--el-input-width: 300px !important;
143166
}
144167
</style>

0 commit comments

Comments
 (0)