Skip to content

Commit 10e0d42

Browse files
shwstpprCopilot
andauthored
ui: introduce section-level “advisories” with quick-fix actions (#11763)
* ui: introduce section-level “advisories” with quick-fix actions This change adds a lightweight “advisories” mechanism to section configs and ships the first advisory to help operators satisfy some of the CKS prerequisites. Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> * fix Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> * fix endpoint.url check Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> * label consistency Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> * Update ui/src/components/view/AdvisoriesView.vue Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * improvements Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> * remove comments Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> * allow disabling Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> --------- Signed-off-by: Abhishek Kumar <abhishek.mrt22@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 98debd2 commit 10e0d42

File tree

7 files changed

+452
-1
lines changed

7 files changed

+452
-1
lines changed

ui/public/locales/en.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,10 @@
292292
"label.add.isolated.network": "Add Isolated Network",
293293
"label.add.kubernetes.cluster": "Add Kubernetes Cluster",
294294
"label.add.acl.name": "ACL name",
295+
"label.add.latest.kubernetes.iso": "Add latest Kubernetes ISO",
295296
"label.add.ldap.account": "Add LDAP Account",
296297
"label.add.logical.router": "Add Logical Router to this Network",
298+
"label.add.minimum.required.compute.offering": "Add minimum required Compute Offering",
297299
"label.add.more": "Add more",
298300
"label.add.nodes": "Add Nodes to Kubernetes Cluster",
299301
"label.add.netscaler.device": "Add Netscaler Device",
@@ -1102,6 +1104,7 @@
11021104
"label.firstname": "First name",
11031105
"label.firstname.lower": "firstname",
11041106
"label.fix.errors": "Fix errors",
1107+
"label.fix.global.setting": "Fix Global Setting",
11051108
"label.fixed": "Fixed Offering",
11061109
"label.for": "for",
11071110
"label.forcks": "For CKS",
@@ -1135,6 +1138,9 @@
11351138
"label.globo.dns.configuration": "GloboDNS configuration",
11361139
"label.glustervolume": "Volume",
11371140
"label.go.back": "Go back",
1141+
"label.go.to.compute.offerings": "Go to Compute Offerings",
1142+
"label.go.to.global.settings": "Go to Global Settings",
1143+
"label.go.to.kubernetes.isos": "Go to Kubernetes ISOs",
11381144
"label.gpu": "GPU",
11391145
"label.gpucardid": "GPU Card",
11401146
"label.gpucardname": "GPU Card",
@@ -3058,12 +3064,17 @@
30583064
"message.add.ip.v6.firewall.rule.failed": "Failed to add IPv6 firewall rule",
30593065
"message.add.ip.v6.firewall.rule.processing": "Adding IPv6 firewall rule...",
30603066
"message.add.ip.v6.firewall.rule.success": "Added IPv6 firewall rule",
3067+
"message.advisory.cks.endpoint.url.not.configured": "Endpoint URL which will be used by Kubernetes clusters is not configured correctly",
3068+
"message.advisory.cks.min.offering": "No suitable Compute Offering found for Kubernetes cluster nodes with minimum required resources (2 vCPU, 2 GB RAM)",
3069+
"message.advisory.cks.version.check": "No Kubernetes version found that can be used to deploy a Kubernetes cluster",
30613070
"message.redeliver.webhook.delivery": "Redeliver this Webhook delivery",
30623071
"message.remove.ip.v6.firewall.rule.failed": "Failed to remove IPv6 firewall rule",
30633072
"message.remove.ip.v6.firewall.rule.processing": "Removing IPv6 firewall rule...",
30643073
"message.remove.ip.v6.firewall.rule.success": "Removed IPv6 firewall rule",
30653074
"message.remove.sslcert.failed": "Failed to remove SSL certificate from load balancer",
30663075
"message.remove.sslcert.processing": "Removing SSL certificate from load balancer...",
3076+
"message.add.latest.kubernetes.iso.failed": "Failed to add latest Kubernetes ISO",
3077+
"message.add.minimum.required.compute.offering.kubernetes.cluster.failed": "Failed to add minimum required Compute Offering for Kubernetes cluster nodes",
30673078
"message.add.netris.controller": "Add Netris Provider",
30683079
"message.add.nsx.controller": "Add NSX Provider",
30693080
"message.add.network": "Add a new network for Zone: <b><span id=\"zone_name\"></span></b>",
@@ -3104,9 +3115,13 @@
31043115
"message.add.vpn.gateway": "Please confirm that you want to add a VPN Gateway.",
31053116
"message.add.vpn.gateway.failed": "Adding VPN gateway failed",
31063117
"message.add.vpn.gateway.processing": "Adding VPN gateway...",
3118+
"message.added.latest.kubernetes.iso": "Latest Kubernetes ISO added successfully",
3119+
"message.added.minimum.required.compute.offering.kubernetes.cluster": "Minimum required Compute Offering for Kubernetes cluster nodes added successfully",
31073120
"message.added.vpc.offering": "Added VPC offering",
31083121
"message.adding.firewall.policy": "Adding Firewall Policy",
31093122
"message.adding.host": "Adding host",
3123+
"message.adding.latest.kubernetes.iso": "Adding latest Kubernetes ISO",
3124+
"message.adding.minimum.required.compute.offering.kubernetes.cluster": "Adding minimum required Compute Offering for Kubernetes cluster nodes",
31103125
"message.adding.netscaler.device": "Adding Netscaler device",
31113126
"message.adding.netscaler.provider": "Adding Netscaler provider",
31123127
"message.adding.nodes.to.cluster": "Adding nodes to Kubernetes cluster",
@@ -3544,6 +3559,8 @@
35443559
"message.failed.to.remove": "Failed to remove",
35453560
"message.forgot.password.success": "An email has been sent to your email address with instructions on how to reset your password.",
35463561
"message.generate.keys": "Please confirm that you would like to generate new API/Secret keys for this User.",
3562+
"message.global.setting.updated": "Global Setting updated successfully.",
3563+
"message.global.setting.update.failed": "Failed to update Global Setting.",
35473564
"message.chart.statistic.info": "The shown charts are self-adjustable, that means, if the value gets close to the limit or overpass it, it will grow to adjust the shown value",
35483565
"message.chart.statistic.info.hypervisor.additionals": "The metrics data depend on the hypervisor plugin used for each hypervisor. The behavior can vary across different hypervisors. For instance, with KVM, metrics are real-time statistics provided by libvirt. In contrast, with VMware, the metrics are averaged data for a given time interval controlled by configuration.",
35493566
"message.guest.traffic.in.advanced.zone": "Guest Network traffic is communication between end-user Instances. Specify a range of VLAN IDs or VXLAN Network identifiers (VNIs) to carry guest traffic for each physical Network.",

ui/src/api/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,7 @@ export function oauthlogin (arg) {
140140
}
141141
})
142142
}
143+
144+
export function getBaseUrl () {
145+
return vueProps.axios.defaults.baseURL
146+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
<template>
19+
<div>
20+
<div v-for="advisory in advisories" :key="advisory.id" style="margin-bottom: 10px;">
21+
<a-alert
22+
:type="advisory.severity || 'info'"
23+
:show-icon="true"
24+
:closable="true"
25+
:message="$t(advisory.message)"
26+
@close="onAlertClose(advisory)">
27+
<template #description>
28+
<a-space direction="horizontal" size="small">
29+
<span v-for="(action, idx) in advisory.actions" :key="idx">
30+
<a-button
31+
v-if="typeof action.show === 'function' ? action.show($store) : action.show"
32+
size="small"
33+
:type="(action.primary || advisory.actions.length === 1) ? 'primary' : 'default'"
34+
@click="onAlertBtnClick(action, advisory)">
35+
{{ $t(action.label) }}
36+
</a-button>
37+
</span>
38+
</a-space>
39+
</template>
40+
</a-alert>
41+
</div>
42+
</div>
43+
</template>
44+
45+
<script>
46+
47+
const DISMISSED_ADVISORIES_KEY = 'dismissed_advisories'
48+
49+
export default {
50+
name: 'AdvisoriesView',
51+
components: {
52+
},
53+
props: {},
54+
data () {
55+
return {
56+
advisories: []
57+
}
58+
},
59+
created () {
60+
this.evaluateAdvisories()
61+
},
62+
computed: {
63+
},
64+
methods: {
65+
async evaluateAdvisories () {
66+
this.advisories = []
67+
const metaAdvisories = this.$route.meta.advisories || []
68+
const dismissedAdvisories = this.$localStorage.get(DISMISSED_ADVISORIES_KEY) || []
69+
const advisoryPromises = metaAdvisories.map(async advisory => {
70+
if (dismissedAdvisories.includes(advisory.id)) {
71+
return null
72+
}
73+
const active = await Promise.resolve(advisory.condition(this.$store))
74+
if (active) {
75+
return advisory
76+
} else if (advisory.dismissOnConditionFail) {
77+
this.dismissAdvisory(advisory.id, true)
78+
}
79+
return null
80+
})
81+
const results = await Promise.all(advisoryPromises)
82+
this.advisories = results.filter(a => a !== null)
83+
},
84+
onAlertClose (advisory) {
85+
this.dismissAdvisory(advisory.id)
86+
},
87+
dismissAdvisory (advisoryId, skipUpdateLocal) {
88+
let dismissedAdvisories = this.$localStorage.get(DISMISSED_ADVISORIES_KEY) || []
89+
dismissedAdvisories = dismissedAdvisories.filter(id => id !== advisoryId)
90+
dismissedAdvisories.push(advisoryId)
91+
this.$localStorage.set(DISMISSED_ADVISORIES_KEY, dismissedAdvisories)
92+
if (skipUpdateLocal) {
93+
return
94+
}
95+
this.advisories = this.advisories.filter(advisory => advisory.id !== advisoryId)
96+
},
97+
undismissAdvisory (advisory, evaluate) {
98+
let dismissedAdvisories = this.$localStorage.get(DISMISSED_ADVISORIES_KEY) || []
99+
dismissedAdvisories = dismissedAdvisories.filter(id => id !== advisory.id)
100+
this.$localStorage.set(DISMISSED_ADVISORIES_KEY, dismissedAdvisories)
101+
if (evaluate) {
102+
Promise.resolve(advisory.condition(this.$store)).then(active => {
103+
if (active) {
104+
this.advisories.push(advisory)
105+
}
106+
})
107+
} else {
108+
this.advisories.push(advisory)
109+
}
110+
},
111+
handleAdvisoryActionError (action, advisory, evaluate) {
112+
if (action.errorMessage) {
113+
this.showActionMessage('error', advisory.id, action.errorMessage)
114+
}
115+
this.undismissAdvisory(advisory, evaluate)
116+
},
117+
handleAdvisoryActionResult (action, advisory, result) {
118+
if (result && action.successMessage) {
119+
this.showActionMessage('success', advisory.id, action.successMessage)
120+
return
121+
}
122+
this.handleAdvisoryActionError(action, advisory, false)
123+
},
124+
showActionMessage (type, key, content) {
125+
const data = {
126+
content: this.$t(content),
127+
key: key,
128+
duration: type === 'loading' ? 0 : 3
129+
}
130+
if (type === 'loading') {
131+
this.$message.loading(data)
132+
} else if (type === 'success') {
133+
this.$message.success(data)
134+
} else if (type === 'error') {
135+
this.$message.error(data)
136+
} else {
137+
this.$message.info(data)
138+
}
139+
},
140+
onAlertBtnClick (action, advisory) {
141+
this.dismissAdvisory(advisory.id)
142+
if (typeof action.run !== 'function') {
143+
return
144+
}
145+
if (action.loadingLabel) {
146+
this.showActionMessage('loading', advisory.id, action.loadingLabel)
147+
}
148+
const result = action.run(this.$store, this.$router)
149+
if (result instanceof Promise) {
150+
result.then(success => {
151+
this.handleAdvisoryActionResult(action, advisory, success)
152+
}).catch(() => {
153+
this.handleAdvisoryActionError(action, advisory, true)
154+
})
155+
} else {
156+
this.handleAdvisoryActionResult(action, advisory, result)
157+
}
158+
}
159+
}
160+
}
161+
</script>

ui/src/config/router.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ function generateRouterMap (section) {
8181
filters: child.filters,
8282
params: child.params ? child.params : {},
8383
columns: child.columns,
84+
advisories: !vueProps.$config.advisoriesDisabled ? child.advisories : undefined,
8485
details: child.details,
8586
searchFilters: child.searchFilters,
8687
related: child.related,
@@ -180,6 +181,10 @@ function generateRouterMap (section) {
180181
map.meta.columns = section.columns
181182
}
182183

184+
if (!vueProps.$config.advisoriesDisabled && section.advisories) {
185+
map.meta.advisories = section.advisories
186+
}
187+
183188
if (section.actions) {
184189
map.meta.actions = section.actions
185190
}

0 commit comments

Comments
 (0)