Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions demo-smart/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,16 @@
>cc-addon-info.smart-metabase</a
>
</li>
<li>
<a class="definition-link" href="?definition=cc-addon-header.smart-kubernetes&smart-mode=kubernetes">
cc-addon-header.smart-kubernetes
</a>
</li>
<li>
<a class="definition-link" href="?definition=cc-addon-info.smart-kubernetes&smart-mode=kubernetes">
cc-addon-info.smart-kubernetes
</a>
</li>
</ul>

<div class="context-buttons">
Expand Down Expand Up @@ -341,6 +351,21 @@
>
addon-otoroshi
</button>
<button
data-context='{
"ownerId":"orga_3547a882-d464-4c34-8168-add4b3e0c135",
"clusterId":"kubernetes_01K85MNF5GTQNR7RWZ3G6RGRZA",
"logsUrlPattern":"/organisations/orga_3547a882-d464-4c34-8168-add4b3e0c135/applications/:id/logs",
"grafanaLink": {
"base": "https://grafana.services.clever-cloud.com",
"console": "https://console.clever-cloud.com"
},
"appOverviewUrlPattern": "/organisations/orga_3547a882-d464-4c34-8168-add4b3e0c135/applications/:id",
"addonDashboardUrlPattern": "/organisations/orga_3547a882-d464-4c34-8168-addons/:id"
}'
>
addon-kubernetes
</button>
</div>

<cc-smart-container> </cc-smart-container>
Expand Down
9 changes: 7 additions & 2 deletions src/components/cc-addon-header/cc-addon-header.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const STATUS_ICON = {
deploying: iconDeploying,
active: iconActive,
failed: iconDeploymentFailed,
deleted: null,
};

/** @type {Partial<CcAddonHeaderStateLoaded>} */
Expand Down Expand Up @@ -155,9 +156,13 @@ export class CcAddonHeader extends LitElement {
</cc-link>
`,
)}
${!isStringEmpty(addonInfo.configLink)
${addonInfo.configLink != null
? html`
<cc-link mode="button" href="${addonInfo.configLink}" ?skeleton=${skeleton} download
<cc-link
mode="button"
href="${addonInfo.configLink.href}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: isn't it a breaking change?

?skeleton=${skeleton}
download="${addonInfo.configLink.fileName}"
>${i18n('cc-addon-header.action.get-config')}
</cc-link>
`
Expand Down
191 changes: 191 additions & 0 deletions src/components/cc-addon-header/cc-addon-header.smart-kubernetes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// @ts-expect-error FIXME: remove when clever-client exports types
import { ONE_SECOND } from '@clevercloud/client/esm/with-cache.js';
import { getAssetUrl } from '../../lib/assets-url.js';
import { fakeString } from '../../lib/fake-strings.js';
import { notify, notifyError } from '../../lib/notifications.js';
import { sendToApi } from '../../lib/send-to-api.js';
import { defineSmartComponent } from '../../lib/smart/define-smart-component.js';
import { i18n } from '../../translations/translation.js';
import '../cc-smart-container/cc-smart-container.js';
import './cc-addon-header.js';

const PROVIDER_ID = 'kubernetes';
const FIFTY_MINUTES = 50 * 60 * 1000;

/**
* @typedef {import('./cc-addon-header.js').CcAddonHeader} CcAddonHeader
* @typedef {import('./cc-addon-header.types.js').DeploymentStatus} DeploymentStatus
* @typedef {import('./cc-addon-header.types.js').CcAddonHeaderStateLoaded} CcAddonHeaderStateLoaded
* @typedef {import('./cc-addon-header.types.js').CcAddonHeaderStateLoading} CcAddonHeaderStateLoading
* @typedef {import('./cc-addon-header.types.js').KubeInfo} KubeInfo
* @typedef {import('../cc-zone/cc-zone.types.js').ZoneStateLoaded} ZoneStateLoaded
* @typedef {import('../../lib/smart/smart-component.types.js').OnContextUpdateArgs<CcAddonHeader>} OnContextUpdateArgs
* @typedef {import('../../lib/send-to-api.js').ApiConfig} ApiConfig
*/

defineSmartComponent({
selector: 'cc-addon-header[smart-mode=kubernetes]',
params: {
apiConfig: { type: Object },
ownerId: { type: String },
clusterId: { type: String },
productStatus: { type: String, optional: true },
},

/** @param {OnContextUpdateArgs} args */
onContextUpdate({ context, updateComponent, signal }) {
const { apiConfig, ownerId, clusterId, productStatus } = context;
const api = new Api({ apiConfig, ownerId, clusterId, signal });

updateComponent('state', {
type: 'loading',
configLink: {
href: fakeString(15),
fileName: fakeString(15),
},
productStatus: fakeString(4),
});

// clear when the component handled by the smart is disconnected from the DOM
const kubeConfigFetchInterval = setInterval(() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: should we add an error toast when this fetch fails ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be nice, error that encourages to refresh the page maybe.

api
.getKubeConfig()
.then((kubeConfigUrl) => {
updateComponent(
'state',
/** @param {CcAddonHeaderStateLoaded|CcAddonHeaderStateLoading} state */
(state) => {
state.configLink = {
fileName: 'kubeconfig.yaml',
href: kubeConfigUrl,
};
},
);
})
.catch((error) => {
console.error(error);
notify({
intent: 'danger',
message: i18n('cc-addon-header.error.fetch-kubeconfig'),
options: {
timeout: 0,
closeable: true,
},
});
updateComponent('state', {
type: 'error',
});
});
}, FIFTY_MINUTES);

signal.addEventListener('abort', () => {
clearInterval(kubeConfigFetchInterval);
});

api
.getKubeInfoWithKubeConfig()
.then(({ kubeInfo, kubeConfigUrl, zone }) => {
updateComponent('state', {
type: 'loaded',
providerId: PROVIDER_ID,
providerLogoUrl: getAssetUrl('/logos/kubernetes.svg'),
name: kubeInfo.name,
id: kubeInfo.id,
zone,
configLink: {
href: kubeConfigUrl,
fileName: 'kubeconfig.yaml',
},
productStatus,
deploymentStatus: /** @type {DeploymentStatus} */ (kubeInfo.status.toLowerCase()),
});
})
.catch((error) => {
console.error(error);
notifyError(i18n('cc-addon-header.error'));
updateComponent('state', {
type: 'error',
});
});
},
});

class Api {
/**
* @param {object} params
* @param {ApiConfig} params.apiConfig - API configuration
* @param {string} params.ownerId - Owner identifier
* @param {string} params.clusterId - Cluster identifier
* @param {AbortSignal} params.signal - Signal to abort calls
*/
constructor({ apiConfig, ownerId, clusterId, signal }) {
this._apiConfig = apiConfig;
this._ownerId = ownerId;
this._clusterId = clusterId;
this._signal = signal;
}

/** @returns {Promise<KubeInfo>} */
_getKubeInfo() {
return getKubeInfo({ ownerId: this._ownerId, clusterId: this._clusterId })
.then(sendToApi({ apiConfig: this._apiConfig, signal: this._signal, cacheDelay: ONE_SECOND }))
.then((kubeInfo) => {
if (kubeInfo.status === 'DELETED') {
throw new Error('This cluster has been deleted');
}
Comment on lines +133 to +135
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to myself for tomorrow: should also filter out DELETING... :/

return kubeInfo;
});
}

/**
* @return {Promise<string>}
*/
getKubeConfig() {
return getKubeConfig({ ownerId: this._ownerId, clusterId: this._clusterId })
.then(sendToApi({ apiConfig: this._apiConfig, signal: this._signal }))
.then(({ url }) => url);
}

async getKubeInfoWithKubeConfig() {
const kubeInfo = await this._getKubeInfo();
const kubeConfigUrl = await this.getKubeConfig();
/** @type ZoneStateLoaded */
const zone = {
type: 'loaded',
name: 'par',
country: 'France',
countryCode: 'FR',
city: 'Paris',
displayName: null,
lat: 48.8566,
lon: 2.3522,
tags: ['for:applications', 'for:par-only', 'infra:clever-cloud'],
};

return { kubeInfo, kubeConfigUrl, zone };
}
}

// FIXME: remove and use the clever-client call from the new clever-client
/** @param {{ ownerId: string, clusterId: string }} params */
function getKubeInfo(params) {
// no multipath for /self or /organisations/{id}
return Promise.resolve({
method: 'get',
url: `/v4/kubernetes/organisations/${params.ownerId}/clusters/${params.clusterId}`,
// no queryParams
// no body
});
}

// FIXME: remove and use the clever-client call from the new clever-client
/** @param {{ ownerId: string, clusterId: string }} params */
function getKubeConfig(params) {
// no multipath for /self or /organisations/{id}
return Promise.resolve({
method: 'get',
url: `/v4/kubernetes/organisations/${params.ownerId}/clusters/${params.clusterId}/kubeconfig/presigned-url`,
// no queryParams
// no body
});
}
60 changes: 60 additions & 0 deletions src/components/cc-addon-header/cc-addon-header.smart-kubernetes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
kind: '🛠 Addon/<cc-addon-header>'
title: '💡 Smart (Kubernetes)'
---
# 💡 Smart `<cc-addon-header smart-mode="kubernetes">`

## ℹ️ Details

<table>
<tr><td><strong>Component </strong> <td><a href="🛠-addons-cc-addon-header--default-story"><code>&lt;cc-addon-header&gt;</code></a>
<tr><td><strong>Selector </strong> <td><code>cc-addon-header[smart-mode="kubernetes"]</code>
<tr><td><strong>Requires auth</strong> <td>Yes
</table>

## ⚙️ Params

| Name | Type | Details | Default |
|------------------|-------------|------------------------------------------------------------------------------------------------|----------|
| `apiConfig` | `ApiConfig` | Object with API configuration (target host, tokens...) | |
| `ownerId` | `string` | UUID prefixed with orga_ | |
| `clusterId` | `string` | ID of the Kubernetes cluster prefixed with kubernetes_ | |
| `productStatus` | `string` | Maturity status of the product | Optional |


```ts
interface ApiConfig {
API_HOST: string,
API_OAUTH_TOKEN: string,
API_OAUTH_TOKEN_SECRET: string,
OAUTH_CONSUMER_KEY: string,
OAUTH_CONSUMER_SECRET: string,
}
```

## 🌐 API endpoints

| Method | URL | Cache? |
| ---------- | -------------------------------------------------------------------------------------------- | ------------------------------- |
| `GET` | `/v4/kubernetes/organisations/${ownerId}/clusters/${clusterId}` | Default |
| `GET` | `/v4/kubernetes/organisations/${ownerId}/clusters/${clusterId}/kubeconfig/presigned-url` | Default (fetched every 50 mins) |


## ⬇️️ Examples

```html
<cc-smart-container context='{
"apiConfig": {
API_HOST: "",
API_OAUTH_TOKEN: "",
API_OAUTH_TOKEN_SECRET: "",
OAUTH_CONSUMER_KEY: "",
OAUTH_CONSUMER_SECRET: "",
},
"ownerId": "orga_XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"clusterId": "kubernetes_XXXXXXXXXXXXXXXXXXXXXXX",
"productStatus": "",
}'>
<cc-addon-header smart-mode="kubernetes"></cc-addon-header>
</cc-smart-container>
```
18 changes: 16 additions & 2 deletions src/components/cc-addon-header/cc-addon-header.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,18 @@ interface OptionalProperties {
};
productStatus?: string;
deploymentStatus?: DeploymentStatus;
configLink?: string;
configLink?: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: isn't it a breaking change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically yes, but since we have only created this prop for Kubernetes, we though we could leave it as it is, but if others think we should make it a proper breaking change, we will do so !

href: string;
fileName: string;
};
}

interface OpenLink {
url: string;
name: string;
}

export type DeploymentStatus = 'deploying' | 'active' | 'failed';
export type DeploymentStatus = 'deploying' | 'active' | 'failed' | 'deleted';

export interface CcAddonHeaderStateLoading extends OptionalProperties {
type: 'loading';
Expand Down Expand Up @@ -68,3 +71,14 @@ export interface RawAddon {
}

export type Addon = BaseProperties & OptionalProperties;

export interface KubeInfo {
id: string;
tenantId: string;
name: string;
description?: string | null;
tag?: string | null;
status: string;
creationDate: string;
version: string;
}
Loading