Merge branch 'metadata' into service-test
diff --git a/dubbo-admin-backend/src/main/java/org/apache/dubbo/admin/controller/ServiceTestController.java b/dubbo-admin-backend/src/main/java/org/apache/dubbo/admin/controller/ServiceTestController.java
new file mode 100644
index 0000000..dfe14c4
--- /dev/null
+++ b/dubbo-admin-backend/src/main/java/org/apache/dubbo/admin/controller/ServiceTestController.java
@@ -0,0 +1,10 @@
+package org.apache.dubbo.admin.controller;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/{env}/test")
+public class ServiceTestController {
+    
+}
diff --git a/dubbo-admin-frontend/package.json b/dubbo-admin-frontend/package.json
index e4a3919..26e89c6 100644
--- a/dubbo-admin-frontend/package.json
+++ b/dubbo-admin-frontend/package.json
@@ -15,6 +15,7 @@
     "brace": "^0.11.1",
     "http-status": "^1.2.0",
     "js-yaml": "^3.12.0",
+    "jsoneditor": "^5.26.2",
     "vue": "^2.5.2",
     "vue-router": "^3.0.1",
     "vuetify": "^1.2.2",
diff --git a/dubbo-admin-frontend/src/components/ServiceTest.vue b/dubbo-admin-frontend/src/components/ServiceTest.vue
new file mode 100644
index 0000000..d4711cd
--- /dev/null
+++ b/dubbo-admin-frontend/src/components/ServiceTest.vue
@@ -0,0 +1,99 @@
+<!--
+  - Licensed to the Apache Software Foundation (ASF) under one or more
+  - contributor license agreements.  See the NOTICE file distributed with
+  - this work for additional information regarding copyright ownership.
+  - The ASF licenses this file to You 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.
+  -->
+<template>
+  <v-container grid-list-xl fluid>
+    <v-layout row wrap>
+      <v-flex xs12>
+        <search v-model="filter" label="Search by service name" :submit="search"></search>
+      </v-flex>
+    </v-layout>
+
+    <v-card>
+      <v-card-text>
+        <v-layout row>
+          <v-flex xs6>
+            <v-data-table :headers="headers" :items="methods" hide-actions>
+              <template slot="items" slot-scope="props">
+                <td>
+                  <div>Name: {{ props.item.name }}</div>
+                  <div>Return: {{ props.item.returnType }}</div>
+                </td>
+                <td></td>
+              </template>
+            </v-data-table>
+          </v-flex>
+          <v-flex xs6>
+            <json-editor v-model="json" />
+          </v-flex>
+        </v-layout>
+      </v-card-text>
+    </v-card>
+  </v-container>
+</template>
+
+<script>
+  import JsonEditor from '@/components/public/JsonEditor'
+  import Search from '@/components/public/Search'
+
+  export default {
+    name: 'ServiceTest',
+    data () {
+      return {
+        filter: null,
+        headers: [
+          {
+            text: 'Method',
+            value: 'name',
+            align: 'left'
+          },
+          {
+            text: 'Operation',
+            value: 'operation',
+            sortable: false,
+            width: '115px'
+          }
+        ],
+        service: null,
+        methods: [],
+        json: {}
+      }
+    },
+    methods: {
+      search () {
+        if (this.filter == null) {
+          this.filter = ''
+        }
+        this.$router.push({
+          path: 'test',
+          query: { service: this.filter }
+        })
+        this.$axios.get('/service/' + this.filter).then(response => {
+          this.service = response.data
+          if (this.service.hasOwnProperty('metadata')) {
+            this.methods = this.service.metadata.methods
+          }
+        }).catch(error => {
+          this.showSnackbar('error', error.response.data.message)
+        })
+      }
+    },
+    components: {
+      JsonEditor,
+      Search
+    }
+  }
+</script>
diff --git a/dubbo-admin-frontend/src/components/public/Footers.vue b/dubbo-admin-frontend/src/components/public/Footers.vue
index a5bbab6..331bea7 100644
--- a/dubbo-admin-frontend/src/components/public/Footers.vue
+++ b/dubbo-admin-frontend/src/components/public/Footers.vue
@@ -16,7 +16,7 @@
   -->
 
 <template>
-  <v-footer inset height="auto" class="pa-3 footer-border-top" app>
+  <v-footer inset height="auto" class="pa-3 footer-border-top">
     <v-spacer></v-spacer>
     <span class="caption mr-1"><strong>Copyright</strong> &copy;{{ new Date().getFullYear() }} <strong>The Apache Software Foundation.</strong></span>
   </v-footer>
diff --git a/dubbo-admin-frontend/src/components/public/JsonEditor.vue b/dubbo-admin-frontend/src/components/public/JsonEditor.vue
new file mode 100644
index 0000000..2891879
--- /dev/null
+++ b/dubbo-admin-frontend/src/components/public/JsonEditor.vue
@@ -0,0 +1,83 @@
+<!--
+  - Licensed to the Apache Software Foundation (ASF) under one or more
+  - contributor license agreements.  See the NOTICE file distributed with
+  - this work for additional information regarding copyright ownership.
+  - The ASF licenses this file to You 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.
+  -->
+<template>
+  <div class="jsoneditor-vue-container"></div>
+</template>
+
+<script>
+  import JSONEditor from 'jsoneditor'
+  import 'jsoneditor/dist/jsoneditor.css'
+
+  export default {
+    name: 'json-editor',
+    props: {
+      value: Object,
+      mode: {
+        type: String,
+        default: 'tree'
+      },
+      modes: {
+        type: Array,
+        default: () => ['tree', 'code']
+      },
+      templates: Array
+    },
+    data () {
+      return {
+        $jsoneditor: null
+      }
+    },
+    watch: {
+      value (newVal, oldVal) {
+        if (newVal !== oldVal && this.$jsoneditor) {
+          this.$jsoneditor.update(newVal)
+        }
+      }
+    },
+    mounted () {
+      const options = {
+        name: 'Parameters',
+        navigationBar: false,
+        mode: this.mode,
+        modes: this.modes,
+        onChange: () => {
+          if (this.$jsoneditor) {
+            var json = this.$jsoneditor.get()
+            this.$emit('input', json)
+          }
+        },
+        templates: this.templates
+      }
+      this.$jsoneditor = new JSONEditor(this.$el, options)
+      this.$jsoneditor.set(this.value)
+      this.$jsoneditor.expandAll()
+    },
+    beforeDestroy () {
+      if (this.$jsoneditor) {
+        this.$jsoneditor.destroy()
+        this.$jsoneditor = null
+      }
+    }
+  }
+</script>
+
+<style scoped>
+  .jsoneditor-vue-container {
+    width: 100%;
+    height: 100%;
+  }
+</style>
diff --git a/dubbo-admin-frontend/src/router/index.js b/dubbo-admin-frontend/src/router/index.js
index 38dc6f0..2fe356e 100644
--- a/dubbo-admin-frontend/src/router/index.js
+++ b/dubbo-admin-frontend/src/router/index.js
@@ -19,6 +19,7 @@
 import Router from 'vue-router'
 import ServiceSearch from '@/components/ServiceSearch'
 import ServiceDetail from '@/components/ServiceDetail'
+import ServiceTest from '@/components/ServiceTest'
 import RoutingRule from '@/components/governance/RoutingRule'
 import TagRule from '@/components/governance/TagRule'
 import AccessControl from '@/components/governance/AccessControl'
@@ -41,6 +42,11 @@
       component: ServiceDetail
     },
     {
+      path: '/test',
+      name: 'ServiceTest',
+      component: ServiceTest
+    },
+    {
       path: '/governance/routingRule',
       name: 'RoutingRule',
       component: RoutingRule