Skip to content

Commit ed28a68

Browse files
d-zlataric-levi9-comjoshiste
authored andcommitted
Add Spring Cloud Gateway Actuator support
closes #993
1 parent 9dab9bf commit ed28a68

File tree

8 files changed

+737
-0
lines changed

8 files changed

+737
-0
lines changed

spring-boot-admin-server-ui/src/main/frontend/components/font-awesome-icon.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ import {faTimesCircle} from '@fortawesome/free-solid-svg-icons/faTimesCircle';
3939
import {faTrash} from '@fortawesome/free-solid-svg-icons/faTrash';
4040
import {faUserCircle} from '@fortawesome/free-solid-svg-icons/faUserCircle';
4141
import {faWrench} from '@fortawesome/free-solid-svg-icons/faWrench';
42+
import {faAngleDoubleRight} from '@fortawesome/free-solid-svg-icons/faAngleDoubleRight';
43+
import {faSearch} from '@fortawesome/free-solid-svg-icons/faSearch';
44+
import {faMapMarker} from '@fortawesome/free-solid-svg-icons/faMapMarker';
45+
import {faFilter} from '@fortawesome/free-solid-svg-icons/faFilter';
4246
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome';
4347

4448
library.add(
@@ -63,6 +67,10 @@ library.add(
6367
faHeartbeat,
6468
faHome,
6569
faWrench,
70+
faAngleDoubleRight,
71+
faSearch,
72+
faMapMarker,
73+
faFilter,
6674
//regular,
6775
farTimesCircle,
6876
//brands

spring-boot-admin-server-ui/src/main/frontend/components/sba-panel.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
&__header--sticky {
8383
position: sticky;
8484
background-color: $white;
85+
z-index: 1000;
8586
}
8687
}
8788
</style>

spring-boot-admin-server-ui/src/main/frontend/services/instance.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,36 @@ class Instance {
127127
});
128128
}
129129

130+
async fetchGlobalFiltersData() {
131+
return this.axios.get(uri`actuator/gateway/globalfilters`, {
132+
headers: {'Accept': actuatorMimeTypes}
133+
});
134+
}
135+
136+
async addGatewayRoute(route) {
137+
return this.axios.post(uri`actuator/gateway/routes/${route.id}`, route, {
138+
headers: {'Content-Type': 'application/json'}
139+
});
140+
}
141+
142+
async fetchRoutesData() {
143+
return this.axios.get(uri`actuator/gateway/routes`, {
144+
headers: {'Accept': actuatorMimeTypes}
145+
});
146+
}
147+
148+
async deleteRoute(routeId) {
149+
return this.axios.delete(uri`actuator/gateway/routes/${routeId}`, {
150+
headers: {'Accept': actuatorMimeTypes}
151+
});
152+
}
153+
154+
async clearRoutesCache() {
155+
return this.axios.post(uri`actuator/gateway/refresh`, {
156+
headers: {'Accept': actuatorMimeTypes}
157+
});
158+
}
159+
130160
async fetchCaches() {
131161
return this.axios.get(uri`actuator/caches`, {
132162
headers: {'Accept': actuatorMimeTypes}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<!--
2+
- Copyright 2014-2018 the original author or authors.
3+
-
4+
- Licensed under the Apache License, Version 2.0 (the "License");
5+
- you may not use this file except in compliance with the License.
6+
- You may obtain a copy of the License at
7+
-
8+
- http://www.apache.org/licenses/LICENSE-2.0
9+
-
10+
- Unless required by applicable law or agreed to in writing, software
11+
- distributed under the License is distributed on an "AS IS" BASIS,
12+
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
- See the License for the specific language governing permissions and
14+
- limitations under the License.
15+
-->
16+
17+
<template>
18+
<div :class="{ 'is-loading' : !hasLoaded }">
19+
<div v-if="error" class="message is-danger">
20+
<div class="message-body">
21+
<strong>
22+
<FontAwesomeIcon class="has-text-danger" icon="exclamation-triangle" />
23+
Fetching global filters failed.
24+
</strong>
25+
<p v-text="error.message" />
26+
</div>
27+
</div>
28+
29+
<sba-panel :header-sticks-below="['#navigation']" title="Global filters" v-if="hasLoaded">
30+
<div class="field has-addons" v-if="hasGlobalFiltersData">
31+
<p class="control is-expanded">
32+
<input class="input" type="search" placeholder="Search filters by name" v-model="globalFilterSearch">
33+
</p>
34+
<div class="control">
35+
<div class="select">
36+
<select v-model="sort">
37+
<option value="undefined">
38+
- Sort by -
39+
</option>
40+
<option value="name">
41+
Name
42+
</option>
43+
<option value="order">
44+
Order
45+
</option>
46+
</select>
47+
</div>
48+
</div>
49+
</div>
50+
51+
<table class="table is-fullwidth is-hoverable" v-if="globalFilters.length > 0">
52+
<thead>
53+
<th>Filter name</th>
54+
<th>Order</th>
55+
</thead>
56+
<tbody>
57+
<tr v-for="filter in globalFilters" :key="filter.name">
58+
<td>
59+
<span v-text="filter.name" class="is-breakable" /><br>
60+
</td>
61+
<td v-text="filter.order" />
62+
</tr>
63+
</tbody>
64+
</table>
65+
</sba-panel>
66+
</div>
67+
</template>
68+
69+
<script>
70+
import Instance from '@/services/instance';
71+
import {compareBy} from '@/utils/collections';
72+
73+
const globalFilterHasKeyword = (globalFilter, keyword) => {
74+
return globalFilter.name.toString().toLowerCase().includes(keyword);
75+
};
76+
77+
const sortGlobalFilter = (globalFilters, sort) => {
78+
if (sort === 'name') {
79+
return [...globalFilters].sort(compareBy(f => f.name))
80+
} else if (sort === 'order') {
81+
return [...globalFilters].sort(compareBy(f => f.order))
82+
}
83+
return globalFilters;
84+
};
85+
86+
export default {
87+
props: {
88+
instance: {
89+
type: Instance,
90+
required: true
91+
}
92+
},
93+
data: () => ({
94+
hasLoaded: false,
95+
error: null,
96+
globalFiltersData: null,
97+
globalFilterSearch: null,
98+
sort: 'undefined'
99+
}),
100+
computed: {
101+
hasGlobalFiltersData() {
102+
return this.globalFiltersData && this.globalFiltersData.length;
103+
},
104+
globalFilters() {
105+
if (!this.globalFiltersData) {
106+
return [];
107+
}
108+
if (!this.globalFilterSearch) {
109+
return sortGlobalFilter(this.globalFiltersData, this.sort);
110+
}
111+
return sortGlobalFilter(this.globalFiltersData.filter(globalFilter => !this.globalFilterSearch || globalFilterHasKeyword(globalFilter, this.globalFilterSearch.toLowerCase())), this.sort);
112+
}
113+
},
114+
created() {
115+
this.fetchGlobalFiltersData();
116+
},
117+
methods: {
118+
async fetchGlobalFiltersData() {
119+
this.error = null;
120+
try {
121+
const res = await this.instance.fetchGlobalFiltersData();
122+
this.globalFiltersData = Object.keys(res.data)
123+
.map(function (key) {
124+
return {name: key.substr(0, key.indexOf('@')), order: res.data[key]};
125+
});
126+
} catch (error) {
127+
console.warn('Fetching global filters failed:', error);
128+
this.error = error;
129+
}
130+
this.hasLoaded = true;
131+
}
132+
}
133+
}
134+
</script>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<!--
2+
- Copyright 2014-2018 the original author or authors.
3+
-
4+
- Licensed under the Apache License, Version 2.0 (the "License");
5+
- you may not use this file except in compliance with the License.
6+
- You may obtain a copy of the License at
7+
-
8+
- http://www.apache.org/licenses/LICENSE-2.0
9+
-
10+
- Unless required by applicable law or agreed to in writing, software
11+
- distributed under the License is distributed on an "AS IS" BASIS,
12+
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
- See the License for the specific language governing permissions and
14+
- limitations under the License.
15+
-->
16+
17+
<template>
18+
<section class="section">
19+
<global-filters-control :instance="instance" />
20+
<add-route-control :instance="instance" />
21+
<routes-control :instance="instance" />
22+
</section>
23+
</template>
24+
25+
<script>
26+
import Instance from '@/services/instance';
27+
import globalFiltersControl from './filters-global';
28+
import routesControl from './routes';
29+
import addRouteControl from './route-add';
30+
31+
export default {
32+
components: {globalFiltersControl, routesControl, addRouteControl},
33+
props: {
34+
instance: {
35+
type: Instance,
36+
required: true
37+
}
38+
},
39+
install({viewRegistry}) {
40+
viewRegistry.addView({
41+
name: 'instances/gateway',
42+
parent: 'instances',
43+
path: 'gateway',
44+
component: this,
45+
label: 'Gateway',
46+
group: 'Insights',
47+
order: 960,
48+
isEnabled: ({instance}) => instance.hasEndpoint('gateway')
49+
});
50+
}
51+
}
52+
</script>
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<!--
2+
- Copyright 2014-2018 the original author or authors.
3+
-
4+
- Licensed under the Apache License, Version 2.0 (the "License");
5+
- you may not use this file except in compliance with the License.
6+
- You may obtain a copy of the License at
7+
-
8+
- http://www.apache.org/licenses/LICENSE-2.0
9+
-
10+
- Unless required by applicable law or agreed to in writing, software
11+
- distributed under the License is distributed on an "AS IS" BASIS,
12+
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
- See the License for the specific language governing permissions and
14+
- limitations under the License.
15+
-->
16+
17+
<template>
18+
<sba-panel :header-sticks-below="['#navigation']" title="Add Route">
19+
<div v-if="error" class="message is-danger">
20+
<div class="message-body">
21+
<strong>
22+
<font-awesome-icon class="has-text-warning" icon="exclamation-triangle" />
23+
Adding route failed:
24+
</strong>
25+
<p v-text="error" />
26+
</div>
27+
</div>
28+
<div class="field has-addons">
29+
<p class="control is-expanded">
30+
<input class="input" placeholder="Route id" v-model="addRouteData.id" required>
31+
</p>
32+
</div>
33+
<div class="field has-addons">
34+
<p class="control is-expanded">
35+
<textarea rows="4" class="input" placeholder="Predicates" v-model="addRouteData.predicates" required />
36+
</p>
37+
</div>
38+
<div class="field has-addons">
39+
<p class="control is-expanded">
40+
<textarea rows="4" class="input" placeholder="Filters" v-model="addRouteData.filters" />
41+
</p>
42+
</div>
43+
<div class="field has-addons">
44+
<p class="control is-expanded">
45+
<input class="input" placeholder="Uri" v-model="addRouteData.uri" required>
46+
</p>
47+
</div>
48+
<div class="field has-addons">
49+
<p class="control is-expanded">
50+
<input class="input" placeholder="Order" v-model="addRouteData.order" type="number" required>
51+
</p>
52+
</div>
53+
<div class="field is-grouped is-grouped-right">
54+
<div class="control">
55+
<button class="button is-primary" :disabled="!addRouteData" @click="addRoute">
56+
<span>Add route</span>
57+
</button>
58+
</div>
59+
</div>
60+
</sba-panel>
61+
</template>
62+
63+
<script>
64+
import Instance from '@/services/instance';
65+
import {from} from '@/utils/rxjs';
66+
67+
export default {
68+
props: {
69+
instance: {
70+
type: Instance,
71+
required: true
72+
}
73+
},
74+
data: () => ({
75+
error: null,
76+
addRouteData: {
77+
'id': null,
78+
'predicates': null,
79+
'filters': null,
80+
'uri': null,
81+
'order': null
82+
}
83+
}),
84+
methods: {
85+
addRoute() {
86+
const vm = this;
87+
this.addRouteData.predicates = JSON.parse(vm.addRouteData.predicates);
88+
this.addRouteData.filters = JSON.parse(vm.addRouteData.filters);
89+
from(vm.instance.addGatewayRoute(vm.addRouteData))
90+
.subscribe({
91+
complete: () => {
92+
vm.addRouteData = {
93+
'id': null,
94+
'predicates': null,
95+
'filters': null,
96+
'uri': null,
97+
'order': null
98+
};
99+
vm.error = null;
100+
setTimeout(() => vm.$emit('route-added'), 2500);
101+
},
102+
error: (error) => {
103+
this.error = 'Server returned: ' + error.response.status;
104+
}
105+
});
106+
}
107+
}
108+
}
109+
</script>
110+

0 commit comments

Comments
 (0)