Skip to content

Commit

Permalink
feat(cc-pricing-product)!: update styles, smart, and state
Browse files Browse the repository at this point in the history
BREAKING CHANGE: see details below
- add new way of handling the component state and passing data to the component.
- remove the heading, desc and image/logo slots.
- use the new smart API.
- directly render a table within the component instead of relying on a
 `cc-pricing-table` sub-component to do so.
- merge the `cc-pricing-table` component and stories into `cc-pricing-product`.
  • Loading branch information
florian-sanders-cc committed Jun 20, 2023
1 parent f739db5 commit 5b79751
Show file tree
Hide file tree
Showing 12 changed files with 946 additions and 514 deletions.
639 changes: 480 additions & 159 deletions src/components/cc-pricing-product/cc-pricing-product.js

Large diffs are not rendered by default.

81 changes: 31 additions & 50 deletions src/components/cc-pricing-product/cc-pricing-product.smart-addon.js
Original file line number Diff line number Diff line change
@@ -1,71 +1,52 @@
import './cc-pricing-product.js';
import '../cc-smart-container/cc-smart-container.js';
import './cc-pricing-product.js';
import { getAllAddonProviders } from '@clevercloud/client/esm/api/v2/product.js';
import { ONE_DAY } from '@clevercloud/client/esm/with-cache.js';
import { fetchPriceSystem } from '../../lib/api-helpers.js';
import { defineSmartComponentWithObservables } from '../../lib/define-smart-component-with-observables.js';
import { LastPromise, unsubscribeWithSignal } from '../../lib/observables.js';
import { defineSmartComponent } from '../../lib/define-smart-component.js';
import { formatAddonProduct } from '../../lib/product.js';
import { sendToApi } from '../../lib/send-to-api.js';

defineSmartComponentWithObservables({
defineSmartComponent({
selector: 'cc-pricing-product[mode="addon"]',
params: {
apiConfig: { type: Object },
addonFeatures: { type: Array },
productId: { type: String },
zoneId: { type: String },
currency: { type: Object },
addonFeatures: { type: Array },
},
onConnect (container, component, context$, disconnectSignal) {

const product_lp = new LastPromise();

unsubscribeWithSignal(disconnectSignal, [

product_lp.error$.subscribe(console.error),
product_lp.error$.subscribe(() => (component.error = true)),
product_lp.value$.subscribe((product) => {
component.name = product.name;
component.icon = product.icon;
component.description = product.description;
component.plans = product.plans;
component.features = product.features;
}),

context$.subscribe(({ productId, zoneId, currency, addonFeatures }) => {

component.error = false;
component.name = null;
component.icon = null;
component.description = null;
component.plans = null;
component.features = null;

if (currency != null) {
component.currency = currency;
}

product_lp.push((signal) => fetchAddonProduct({ signal, productId, zoneId, addonFeatures }));
}),

]);

onContextUpdate ({ context, updateComponent, signal }) {
const { apiConfig, productId, zoneId, addonFeatures } = context;

// Reset the component before loading
updateComponent('state', { state: 'loading' });

fetchAddonProduct({ apiConfig, zoneId, productId, addonFeatures, signal })
.then((productDetails) => {
updateComponent('product', {
state: 'loaded',
name: productDetails.name,
productFeatures: productDetails.productFeatures,
plans: productDetails.plans,
});
})
.catch((error) => {
console.error(error);
updateComponent('product', { state: 'error' });
});
},
});

async function fetchAddonProduct ({ signal, productId, zoneId = 'PAR', addonFeatures }) {

const [addonProvider, priceSystem] = await Promise.all([
fetchAddonProvider({ signal, productId }),
fetchPriceSystem({ signal, zoneId }),
]);

return formatAddonProduct(addonProvider, priceSystem, addonFeatures);
function fetchAddonProduct ({ apiConfig, productId, zoneId, addonFeatures, signal }) {
return Promise.all([
fetchAddonProvider({ apiConfig, productId, signal }),
fetchPriceSystem({ apiConfig, zoneId, signal }),
]).then(([addonProvider, priceSystem]) => formatAddonProduct(addonProvider, priceSystem, addonFeatures));
}

function fetchAddonProvider ({ signal, productId }) {
function fetchAddonProvider ({ apiConfig, signal, productId }) {
return getAllAddonProviders()
.then(sendToApi({ signal, cacheDelay: ONE_DAY }))
.then(sendToApi({ apiConfig, cacheDelay: ONE_DAY, signal }))
.then((allAddonProviders) => {
const addonProvider = allAddonProviders.find((ap) => ap.id === productId);
if (addonProvider == null) {
Expand Down
68 changes: 36 additions & 32 deletions src/components/cc-pricing-product/cc-pricing-product.smart-addon.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,17 @@ title: '💡 Smart (add-on)'

## ⚙️ Params

<table>
<tr><th>Name <th>Type <th>Details <th>Default
<tr><td><code>productId</code> <td><code>String</code> <td>id from <a href="https://api.clever-cloud.com/v2/products/addonproviders"><code>/v2/products/addonproviders</code></a> <td>
<tr><td><code>zoneId</code> <td><code>String</code> <td>Name from <a href="https://api.clever-cloud.com/v4/products/zones"><code>/v4/products/zones</code></a> <td><code>par</code>
<tr><td><code>currency</code> <td><code>Currency</code> <td>Currency info <td><code>{ code: 'EUR', changeRate: 1 }</code>
<tr><td><code>addonFeatures</code> <td><code>String[]</code> <td>List of feature codes as describe in the component API. <td><code>undefined</code>
</table>
| Name | Type | Details | Default |
|-----------------|-------------|--------------------------------------------------------------------------------------------------|---------|
| `apiConfig` | `ApiConfig` | Object with API configuration (only `API_HOST` is required for this component) | |
| `productId` | `string` | id from [`/v2/products/addonproviders`](https://api.clever-cloud.com/v2/products/addonproviders) | |
| `zoneId` | `string` | Name from [`/v4/products/zones`](https://api.clever-cloud.com/v4/products/zones) | `par` |
| `addonFeatures` | `string[]` | List of feature codes as describe in the component API. | |


```ts
interface Currency {
code: string, // ISO 4217 currency code
changeRate: number, // based on euros
interface ApiConfig {
API_HOST: string,
}
```

Expand All @@ -35,55 +34,60 @@ interface Currency {

## 🌐 API endpoints

<table>
<tr><th>Method <th>URL <th>Cache?
<tr><td>GET <td><code>/v2/products/addonproviders</code> <td>1 day
<tr><td>GET <td><code>/v4/billing/price-system?zone_id</code> <td>1 day
</table>
| Method | Type | Cache? |
|--------|:-----------------------------------|--------|
| `GET` | `/v2/products/addonproviders` | 1 day |
| `GET` | `/v4/billing/price-system?zone_id` | 1 day |

## ⬇️️ Examples

### Simple

Simple example based on default zone and currency.
Simple example based on default zone.

```html
<cc-smart-container context='{ "productId": "postgresql-addon" }'>
<cc-smart-container context='{
"apiConfig": {
API_HOST: "",
},
"productId": "postgresql-addon",
}'>
<cc-pricing-product mode="addon"></cc-pricing-product>
</cc-smart-container>
```

<cc-smart-container context='{ "productId": "postgresql-addon" }'>
<cc-pricing-product mode="addon"></cc-pricing-product>
</cc-smart-container>
### Zone

### Zone and currency

Simple example with custom zone and custom currency.
Simple example with custom zone.

NOTE: Prices are the same on all zones right now.

```html
<cc-smart-container context='{ "productId": "postgresql-addon", "zoneId": "rbx", "currency": { "code": "USD", "changeRate": 1.1802 } }'>
<cc-smart-container context='{
"apiConfig": {
API_HOST: "",
},
"productId": "postgresql-addon",
"zoneId": "rbx",
}'>
<cc-pricing-product mode="addon"></cc-pricing-product>
</cc-smart-container>
```

<cc-smart-container context='{ "productId": "postgresql-addon", "zoneId": "rbx", "currency": { "code": "USD", "changeRate": 1.1802 } }'>
<cc-pricing-product mode="addon"></cc-pricing-product>
</cc-smart-container>

### With feature list

Setting `addonFeatures` is the only way to enforce a sort order on the feature list.
It's also a good way to filter features.

```html
<cc-smart-container context='{ "productId": "postgresql-addon", "addonFeatures": ["cpu", "memory", "disk-size"] }'>
<cc-smart-container context='{
"apiConfig": {
API_HOST: "",
},
"productId": "postgresql-addon",
"addonFeatures": ["cpu", "memory", "disk-size"],
}'>
<cc-pricing-product mode="addon"></cc-pricing-product>
</cc-smart-container>
```

<cc-smart-container context='{ "productId": "postgresql-addon", "addonFeatures": ["cpu", "memory", "disk-size"] }'>
<cc-pricing-product mode="addon"></cc-pricing-product>
</cc-smart-container>
Original file line number Diff line number Diff line change
@@ -1,70 +1,51 @@
import './cc-pricing-product.js';
import '../cc-smart-container/cc-smart-container.js';
import { getAvailableInstances } from '@clevercloud/client/esm/api/v2/product.js';
import { ONE_DAY } from '@clevercloud/client/esm/with-cache.js';
import { fetchPriceSystem } from '../../lib/api-helpers.js';
import { defineSmartComponentWithObservables } from '../../lib/define-smart-component-with-observables.js';
import { LastPromise, unsubscribeWithSignal } from '../../lib/observables.js';
import { defineSmartComponent } from '../../lib/define-smart-component.js';
import { formatRuntimeProduct, getRunnerProduct } from '../../lib/product.js';
import { sendToApi } from '../../lib/send-to-api.js';
import './cc-pricing-product.js';

defineSmartComponentWithObservables({
defineSmartComponent({
selector: 'cc-pricing-product[mode="runtime"]',
params: {
apiConfig: { type: Object },
productId: { type: String },
zoneId: { type: String },
currency: { type: Object },
},
onConnect (container, component, context$, disconnectSignal) {

const product_lp = new LastPromise();

unsubscribeWithSignal(disconnectSignal, [

product_lp.error$.subscribe(console.error),
product_lp.error$.subscribe(() => (component.error = true)),
product_lp.value$.subscribe((product) => {
component.name = product.name;
component.icon = product.icon;
component.description = product.description;
component.plans = product.plans;
component.features = product.features;
}),

context$.subscribe(({ productId, zoneId, currency }) => {

component.error = false;
component.name = null;
component.icon = null;
component.description = null;
component.plans = null;
component.features = null;

if (currency != null) {
component.currency = currency;
}

product_lp.push((signal) => fetchRuntimeProduct({ signal, productId, zoneId }));
}),

]);

onContextUpdate ({ context, updateComponent, signal }) {
const { apiConfig, productId, zoneId } = context;

// Reset the component before loading
updateComponent('state', { state: 'loading' });

fetchRuntimeProduct({ apiConfig, productId, zoneId, signal })
.then((productDetails) => {
updateComponent('product', {
state: 'loaded',
name: productDetails.name,
productFeatures: productDetails.productFeatures,
plans: productDetails.plans,
});
})
.catch((error) => {
console.error(error);
updateComponent('product', { state: 'error' });
});
},
});

async function fetchRuntimeProduct ({ signal, productId, zoneId = 'PAR' }) {

const [runtime, priceSystem] = await Promise.all([
fetchRuntime({ signal, productId }),
fetchPriceSystem({ signal, zoneId }),
]);

return formatRuntimeProduct(runtime, priceSystem);
function fetchRuntimeProduct ({ apiConfig, productId, zoneId, signal }) {
return Promise.all([
fetchRuntime({ apiConfig, productId, signal }),
fetchPriceSystem({ zoneId, signal }),
]).then(([runtime, priceSystem]) => formatRuntimeProduct(runtime, priceSystem));
}

function fetchRuntime ({ signal, productId }) {
function fetchRuntime ({ apiConfig, productId, signal }) {
return getAvailableInstances()
.then(sendToApi({ signal, cacheDelay: ONE_DAY }))
.then(sendToApi({ apiConfig, cacheDelay: ONE_DAY, signal }))
.then((allRuntimes) => {
const runtime = allRuntimes.find((f) => f.variant.slug === productId);
if (runtime == null) {
Expand Down

0 comments on commit 5b79751

Please sign in to comment.