Skip to content

Commit b757dee

Browse files
Merge pull request #1588 from CleverCloud/dashboard-addons/kube
feat(cc-addon-*.smart-kubernetes): init
2 parents 784a332 + 6a0d37c commit b757dee

File tree

9 files changed

+488
-23
lines changed

9 files changed

+488
-23
lines changed

demo-smart/index.html

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,16 @@
208208
>cc-addon-info.smart-metabase</a
209209
>
210210
</li>
211+
<li>
212+
<a class="definition-link" href="?definition=cc-addon-header.smart-kubernetes&smart-mode=kubernetes">
213+
cc-addon-header.smart-kubernetes
214+
</a>
215+
</li>
216+
<li>
217+
<a class="definition-link" href="?definition=cc-addon-info.smart-kubernetes&smart-mode=kubernetes">
218+
cc-addon-info.smart-kubernetes
219+
</a>
220+
</li>
211221
</ul>
212222

213223
<div class="context-buttons">
@@ -341,6 +351,21 @@
341351
>
342352
addon-otoroshi
343353
</button>
354+
<button
355+
data-context='{
356+
"ownerId":"orga_3547a882-d464-4c34-8168-add4b3e0c135",
357+
"kubernetesId":"kubernetes_01K85MNF5GTQNR7RWZ3G6RGRZA",
358+
"logsUrlPattern":"/organisations/orga_3547a882-d464-4c34-8168-add4b3e0c135/applications/:id/logs",
359+
"grafanaLink": {
360+
"base": "https://grafana.services.clever-cloud.com",
361+
"console": "https://console.clever-cloud.com"
362+
},
363+
"appOverviewUrlPattern": "/organisations/orga_3547a882-d464-4c34-8168-add4b3e0c135/applications/:id",
364+
"addonDashboardUrlPattern": "/organisations/orga_3547a882-d464-4c34-8168-addons/:id"
365+
}'
366+
>
367+
addon-kubernetes
368+
</button>
344369
</div>
345370

346371
<cc-smart-container> </cc-smart-container>

src/components/cc-addon-header/cc-addon-header.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const STATUS_ICON = {
2525
deploying: iconDeploying,
2626
active: iconActive,
2727
failed: iconDeploymentFailed,
28+
deleted: null,
2829
};
2930

3031
/** @type {Partial<CcAddonHeaderStateLoaded>} */
@@ -72,15 +73,16 @@ export class CcAddonHeader extends LitElement {
7273

7374
/**
7475
* @param {DeploymentStatus} deploymentStatus
76+
* @param {string} providerId
7577
* @returns {string}
7678
* @private
7779
*/
78-
_getStatusMsg(deploymentStatus) {
80+
_getStatusMsg(deploymentStatus, providerId) {
7981
if (deploymentStatus === 'deploying') {
8082
return i18n('cc-addon-header.state-msg.deployment-is-deploying');
8183
}
8284
if (deploymentStatus === 'active') {
83-
return i18n('cc-addon-header.state-msg.deployment-is-active');
85+
return i18n('cc-addon-header.state-msg.deployment-is-active', { providerId });
8486
}
8587
if (deploymentStatus === 'failed') {
8688
return i18n('cc-addon-header.state-msg.deployment-failed');
@@ -114,6 +116,7 @@ export class CcAddonHeader extends LitElement {
114116
const isRestarting = this.state.type === 'restarting';
115117
const isRebuilding = this.state.type === 'rebuilding';
116118
const deploymentStatus = this.state.deploymentStatus;
119+
const providerId = this.state.type === 'loaded' ? this.state.providerId : '';
117120

118121
return html`
119122
<cc-block>
@@ -155,9 +158,13 @@ export class CcAddonHeader extends LitElement {
155158
</cc-link>
156159
`,
157160
)}
158-
${!isStringEmpty(addonInfo.configLink)
161+
${addonInfo.configLink != null
159162
? html`
160-
<cc-link mode="button" href="${addonInfo.configLink}" ?skeleton=${skeleton} download
163+
<cc-link
164+
mode="button"
165+
href="${addonInfo.configLink.href}"
166+
?skeleton=${skeleton}
167+
download="${addonInfo.configLink.fileName}"
161168
>${i18n('cc-addon-header.action.get-config')}
162169
</cc-link>
163170
`
@@ -200,7 +207,7 @@ export class CcAddonHeader extends LitElement {
200207
.icon=${STATUS_ICON[deploymentStatus]}
201208
?skeleton=${skeleton}
202209
></cc-icon>
203-
<span class=${classMap({ skeleton })}> ${this._getStatusMsg(deploymentStatus)} </span>
210+
<span class=${classMap({ skeleton })}> ${this._getStatusMsg(deploymentStatus, providerId)} </span>
204211
`
205212
: ''}
206213
${!isStringEmpty(addonInfo.logsUrl)
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// @ts-expect-error FIXME: remove when clever-client exports types
2+
import { ONE_SECOND } from '@clevercloud/client/esm/with-cache.js';
3+
import { getAssetUrl } from '../../lib/assets-url.js';
4+
import { fakeString } from '../../lib/fake-strings.js';
5+
import { notify, notifyError } from '../../lib/notifications.js';
6+
import { sendToApi } from '../../lib/send-to-api.js';
7+
import { defineSmartComponent } from '../../lib/smart/define-smart-component.js';
8+
import { i18n } from '../../translations/translation.js';
9+
import '../cc-smart-container/cc-smart-container.js';
10+
import './cc-addon-header.js';
11+
12+
const PROVIDER_ID = 'kubernetes';
13+
const FIFTY_MINUTES = 50 * 60 * 1000;
14+
15+
/**
16+
* @typedef {import('./cc-addon-header.js').CcAddonHeader} CcAddonHeader
17+
* @typedef {import('./cc-addon-header.types.js').DeploymentStatus} DeploymentStatus
18+
* @typedef {import('./cc-addon-header.types.js').CcAddonHeaderStateLoaded} CcAddonHeaderStateLoaded
19+
* @typedef {import('./cc-addon-header.types.js').CcAddonHeaderStateLoading} CcAddonHeaderStateLoading
20+
* @typedef {import('./cc-addon-header.types.js').KubeInfo} KubeInfo
21+
* @typedef {import('../cc-zone/cc-zone.types.js').ZoneStateLoaded} ZoneStateLoaded
22+
* @typedef {import('../../lib/smart/smart-component.types.js').OnContextUpdateArgs<CcAddonHeader>} OnContextUpdateArgs
23+
* @typedef {import('../../lib/send-to-api.js').ApiConfig} ApiConfig
24+
*/
25+
26+
defineSmartComponent({
27+
selector: 'cc-addon-header[smart-mode=kubernetes]',
28+
params: {
29+
apiConfig: { type: Object },
30+
ownerId: { type: String },
31+
kubernetesId: { type: String },
32+
productStatus: { type: String, optional: true },
33+
},
34+
35+
/** @param {OnContextUpdateArgs} args */
36+
onContextUpdate({ context, updateComponent, signal }) {
37+
const { apiConfig, ownerId, kubernetesId, productStatus } = context;
38+
const api = new Api({ apiConfig, ownerId, kubernetesId, signal });
39+
40+
updateComponent('state', {
41+
type: 'loading',
42+
configLink: {
43+
href: fakeString(15),
44+
fileName: fakeString(15),
45+
},
46+
productStatus: fakeString(4),
47+
});
48+
49+
// clear when the component handled by the smart is disconnected from the DOM
50+
const kubeConfigFetchInterval = setInterval(() => {
51+
api
52+
.getKubeConfig()
53+
.then((kubeConfigUrl) => {
54+
updateComponent(
55+
'state',
56+
/** @param {CcAddonHeaderStateLoaded|CcAddonHeaderStateLoading} state */
57+
(state) => {
58+
state.configLink = {
59+
fileName: 'kubeconfig.yaml',
60+
href: kubeConfigUrl,
61+
};
62+
},
63+
);
64+
})
65+
.catch((error) => {
66+
console.error(error);
67+
notify({
68+
intent: 'danger',
69+
message: i18n('cc-addon-header.error.fetch-kubeconfig'),
70+
options: {
71+
timeout: 0,
72+
closeable: true,
73+
},
74+
});
75+
updateComponent('state', {
76+
type: 'error',
77+
});
78+
});
79+
}, FIFTY_MINUTES);
80+
81+
signal.addEventListener('abort', () => {
82+
clearInterval(kubeConfigFetchInterval);
83+
});
84+
85+
api
86+
.getKubeInfoWithKubeConfig()
87+
.then(({ kubeInfo, kubeConfigUrl, zone }) => {
88+
updateComponent('state', {
89+
type: 'loaded',
90+
providerId: PROVIDER_ID,
91+
providerLogoUrl: getAssetUrl('/logos/kubernetes.svg'),
92+
name: kubeInfo.name,
93+
id: kubeInfo.id,
94+
zone,
95+
configLink: {
96+
href: kubeConfigUrl,
97+
fileName: 'kubeconfig.yaml',
98+
},
99+
productStatus,
100+
deploymentStatus: /** @type {DeploymentStatus} */ (kubeInfo.status.toLowerCase()),
101+
});
102+
})
103+
.catch((error) => {
104+
console.error(error);
105+
notifyError(i18n('cc-addon-header.error'));
106+
updateComponent('state', {
107+
type: 'error',
108+
});
109+
});
110+
},
111+
});
112+
113+
class Api {
114+
/**
115+
* @param {object} params
116+
* @param {ApiConfig} params.apiConfig - API configuration
117+
* @param {string} params.ownerId - Owner identifier
118+
* @param {string} params.kubernetesId - Cluster identifier
119+
* @param {AbortSignal} params.signal - Signal to abort calls
120+
*/
121+
constructor({ apiConfig, ownerId, kubernetesId, signal }) {
122+
this._apiConfig = apiConfig;
123+
this._ownerId = ownerId;
124+
this._kubernetesId = kubernetesId;
125+
this._signal = signal;
126+
}
127+
128+
/** @returns {Promise<KubeInfo>} */
129+
_getKubeInfo() {
130+
return getKubeInfo({ ownerId: this._ownerId, kubernetesId: this._kubernetesId })
131+
.then(sendToApi({ apiConfig: this._apiConfig, signal: this._signal, cacheDelay: ONE_SECOND }))
132+
.then((kubeInfo) => {
133+
if (kubeInfo.status === 'DELETED' || kubeInfo.status === 'DELETING') {
134+
throw new Error('This cluster has been deleted');
135+
}
136+
return kubeInfo;
137+
});
138+
}
139+
140+
/**
141+
* @return {Promise<string>}
142+
*/
143+
getKubeConfig() {
144+
return getKubeConfig({ ownerId: this._ownerId, kubernetesId: this._kubernetesId })
145+
.then(sendToApi({ apiConfig: this._apiConfig, signal: this._signal }))
146+
.then(({ url }) => url);
147+
}
148+
149+
async getKubeInfoWithKubeConfig() {
150+
const kubeInfo = await this._getKubeInfo();
151+
const kubeConfigUrl = await this.getKubeConfig();
152+
/** @type ZoneStateLoaded */
153+
const zone = {
154+
type: 'loaded',
155+
name: 'par',
156+
country: 'France',
157+
countryCode: 'FR',
158+
city: 'Paris',
159+
displayName: null,
160+
lat: 48.8566,
161+
lon: 2.3522,
162+
tags: ['for:applications', 'for:par-only', 'infra:clever-cloud'],
163+
};
164+
165+
return { kubeInfo, kubeConfigUrl, zone };
166+
}
167+
}
168+
169+
// FIXME: remove and use the clever-client call from the new clever-client
170+
/** @param {{ ownerId: string, kubernetesId: string }} params */
171+
function getKubeInfo(params) {
172+
// no multipath for /self or /organisations/{id}
173+
return Promise.resolve({
174+
method: 'get',
175+
url: `/v4/kubernetes/organisations/${params.ownerId}/clusters/${params.kubernetesId}`,
176+
// no queryParams
177+
// no body
178+
});
179+
}
180+
181+
// FIXME: remove and use the clever-client call from the new clever-client
182+
/** @param {{ ownerId: string, kubernetesId: string }} params */
183+
function getKubeConfig(params) {
184+
// no multipath for /self or /organisations/{id}
185+
return Promise.resolve({
186+
method: 'get',
187+
url: `/v4/kubernetes/organisations/${params.ownerId}/clusters/${params.kubernetesId}/kubeconfig/presigned-url`,
188+
// no queryParams
189+
// no body
190+
});
191+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
kind: '🛠 Addon/<cc-addon-header>'
3+
title: '💡 Smart (Kubernetes)'
4+
---
5+
# 💡 Smart `<cc-addon-header smart-mode="kubernetes">`
6+
7+
## ℹ️ Details
8+
9+
<table>
10+
<tr><td><strong>Component </strong> <td><a href="🛠-addons-cc-addon-header--default-story"><code>&lt;cc-addon-header&gt;</code></a>
11+
<tr><td><strong>Selector </strong> <td><code>cc-addon-header[smart-mode="kubernetes"]</code>
12+
<tr><td><strong>Requires auth</strong> <td>Yes
13+
</table>
14+
15+
## ⚙️ Params
16+
17+
| Name | Type | Details | Default |
18+
| ------------------ | ------------- | ------------------------------------------------------------------------------------------------ | ---------- |
19+
| `apiConfig` | `ApiConfig` | Object with API configuration (target host, tokens...) | |
20+
| `ownerId` | `string` | UUID prefixed with orga_ | |
21+
| `kubernetesId` | `string` | ID of the Kubernetes cluster prefixed with kubernetes_ | |
22+
| `productStatus` | `string` | Maturity status of the product | Optional |
23+
24+
25+
```ts
26+
interface ApiConfig {
27+
API_HOST: string,
28+
API_OAUTH_TOKEN: string,
29+
API_OAUTH_TOKEN_SECRET: string,
30+
OAUTH_CONSUMER_KEY: string,
31+
OAUTH_CONSUMER_SECRET: string,
32+
}
33+
```
34+
35+
## 🌐 API endpoints
36+
37+
| Method | URL | Cache? |
38+
| ---------- | -------------------------------------------------------------------------------------------- | ------------------------------- |
39+
| `GET` | `/v4/kubernetes/organisations/${ownerId}/clusters/${kubernetesId}` | Default |
40+
| `GET` | `/v4/kubernetes/organisations/${ownerId}/clusters/${kubernetesId}/kubeconfig/presigned-url` | Default (fetched every 50 mins) |
41+
42+
43+
## ⬇️️ Examples
44+
45+
```html
46+
<cc-smart-container context='{
47+
"apiConfig": {
48+
API_HOST: "",
49+
API_OAUTH_TOKEN: "",
50+
API_OAUTH_TOKEN_SECRET: "",
51+
OAUTH_CONSUMER_KEY: "",
52+
OAUTH_CONSUMER_SECRET: "",
53+
},
54+
"ownerId": "orga_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
55+
"kubernetesId": "kubernetes_XXXXXXXXXXXXXXXXXXXXXXX",
56+
"productStatus": "",
57+
}'>
58+
<cc-addon-header smart-mode="kubernetes"></cc-addon-header>
59+
</cc-smart-container>
60+
```

src/components/cc-addon-header/cc-addon-header.types.d.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,18 @@ interface OptionalProperties {
2525
};
2626
productStatus?: string;
2727
deploymentStatus?: DeploymentStatus;
28-
configLink?: string;
28+
configLink?: {
29+
href: string;
30+
fileName: string;
31+
};
2932
}
3033

3134
interface OpenLink {
3235
url: string;
3336
name: string;
3437
}
3538

36-
export type DeploymentStatus = 'deploying' | 'active' | 'failed';
39+
export type DeploymentStatus = 'deploying' | 'active' | 'failed' | 'deleted';
3740

3841
export interface CcAddonHeaderStateLoading extends OptionalProperties {
3942
type: 'loading';
@@ -68,3 +71,14 @@ export interface RawAddon {
6871
}
6972

7073
export type Addon = BaseProperties & OptionalProperties;
74+
75+
export interface KubeInfo {
76+
id: string;
77+
tenantId: string;
78+
name: string;
79+
description?: string | null;
80+
tag?: string | null;
81+
status: string;
82+
creationDate: string;
83+
version: string;
84+
}

0 commit comments

Comments
 (0)