Skip to content

Commit

Permalink
Source Quota Metadata from an API (#154)
Browse files Browse the repository at this point in the history
Gives UX people the choice of what they call things etc.
  • Loading branch information
spjmurray authored Feb 3, 2025
1 parent 187e21d commit e89dcc8
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 73 deletions.
1 change: 1 addition & 0 deletions src/lib/openapi/identity/.openapi-generator/FILES
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ models/ProjectWrite.ts
models/ProviderScope.ts
models/Quota.ts
models/QuotaDetailed.ts
models/QuotaMetadata.ts
models/QuotasRead.ts
models/QuotasWrite.ts
models/ResourceMetadata.ts
Expand Down
34 changes: 34 additions & 0 deletions src/lib/openapi/identity/apis/DefaultApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import type {
OrganizationWrite,
ProjectRead,
ProjectWrite,
QuotaMetadata,
QuotasRead,
QuotasWrite,
RoleRead,
Expand Down Expand Up @@ -73,6 +74,8 @@ import {
ProjectReadToJSON,
ProjectWriteFromJSON,
ProjectWriteToJSON,
QuotaMetadataFromJSON,
QuotaMetadataToJSON,
QuotasReadFromJSON,
QuotasReadToJSON,
QuotasWriteFromJSON,
Expand Down Expand Up @@ -1773,6 +1776,37 @@ export class DefaultApi extends runtime.BaseAPI {
return await response.value();
}

/**
* Return a list of quotas and their metadata.
*/
async apiV1QuotasGetRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<Array<QuotaMetadata>>> {
const queryParameters: any = {};

const headerParameters: runtime.HTTPHeaders = {};

if (this.configuration && this.configuration.accessToken) {
// oauth required
headerParameters["Authorization"] = await this.configuration.accessToken("oauth2Authentication", []);
}

const response = await this.request({
path: `/api/v1/quotas`,
method: 'GET',
headers: headerParameters,
query: queryParameters,
}, initOverrides);

return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(QuotaMetadataFromJSON));
}

/**
* Return a list of quotas and their metadata.
*/
async apiV1QuotasGet(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<Array<QuotaMetadata>> {
const response = await this.apiV1QuotasGetRaw(initOverrides);
return await response.value();
}

/**
* Complete the user signup process.
*/
Expand Down
93 changes: 93 additions & 0 deletions src/lib/openapi/identity/models/QuotaMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/* tslint:disable */
/* eslint-disable */
/**
* Unikorn Identity API
* The Unikorn Identity API provides an OIDC compliant interface for use with all Unikorn services and proxies. As it\'s intended use is for multi-tenant cloud deployments it acts as an aggregation layer for other 3rd party OIDC services, dispatching login requests to the required OIDC backend.
*
* The version of the OpenAPI document: 0.1.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/

import { exists, mapValues } from '../runtime';
/**
* A single quota's metadata.
* @export
* @interface QuotaMetadata
*/
export interface QuotaMetadata {
/**
* The internal quota name.
* @type {string}
* @memberof QuotaMetadata
*/
name: string;
/**
* The name that should be displayed to end users.
* @type {string}
* @memberof QuotaMetadata
*/
displayName: string;
/**
* A verbose explanation of what the quota limits.
* @type {string}
* @memberof QuotaMetadata
*/
description: string;
/**
* The default value of the quota.
* @type {number}
* @memberof QuotaMetadata
*/
_default: number;
}

/**
* Check if a given object implements the QuotaMetadata interface.
*/
export function instanceOfQuotaMetadata(value: object): boolean {
let isInstance = true;
isInstance = isInstance && "name" in value;
isInstance = isInstance && "displayName" in value;
isInstance = isInstance && "description" in value;
isInstance = isInstance && "_default" in value;

return isInstance;
}

export function QuotaMetadataFromJSON(json: any): QuotaMetadata {
return QuotaMetadataFromJSONTyped(json, false);
}

export function QuotaMetadataFromJSONTyped(json: any, ignoreDiscriminator: boolean): QuotaMetadata {
if ((json === undefined) || (json === null)) {
return json;
}
return {

'name': json['name'],
'displayName': json['displayName'],
'description': json['description'],
'_default': json['default'],
};
}

export function QuotaMetadataToJSON(value?: QuotaMetadata | null): any {
if (value === undefined) {
return undefined;
}
if (value === null) {
return null;
}
return {

'name': value.name,
'displayName': value.displayName,
'description': value.description,
'default': value._default,
};
}

1 change: 1 addition & 0 deletions src/lib/openapi/identity/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export * from './ProjectWrite';
export * from './ProviderScope';
export * from './Quota';
export * from './QuotaDetailed';
export * from './QuotaMetadata';
export * from './QuotasRead';
export * from './QuotasWrite';
export * from './ResourceMetadata';
Expand Down
78 changes: 31 additions & 47 deletions src/routes/(shell)/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,66 +15,50 @@
name: 'Dashboard',
description: 'Summary of your resources.'
};
// TODO: move into the API.
type QuotaMetadata = {
title: string;
description: string;
};
const metadata: Record<string, QuotaMetadata> = {
clusters: {
title: 'Clusters',
description: 'All cluster types e.g. Kubernetes, compute, etc.'
},
servers: {
title: 'Servers',
description: 'All servers and virtual machines.'
},
gpus: {
title: 'GPUs',
description: 'General purpose GPUs for AI workloads.'
}
};
</script>

<ShellPage {settings}>
<ShellSection title="Resource Utilization">
<div class="flex flex-col lg:flex-row lg:flex-wrap gap-4">
{#each data.quotas.capacity as quota, i}
{@const metadata = data.quotaMetadata.find((x) => x.name == quota.kind)}
{@const used = data.quotas.allocated[i].committed + data.quotas.allocated[i].reserved}
{@const utilization = (used * 100) / quota.quantity}

<div class="flex flex-col gap-4 card bg-surface-50-900-token shadow p-4">
<div class="flex flex-col gap-2">
<div class="h4">{metadata[quota.kind].title}</div>
<div class="italic text-sm text-surface-500">{metadata[quota.kind].description}</div>
</div>
<div class="flex items-center gap-4">
<ProgressRadial
value={utilization}
meter="stroke-primary-500 dark:stroke-primary-50"
track="stroke-primary-500/20"
stroke={48}
font={112}
>
{Math.round(utilization)}%
</ProgressRadial>
{#if metadata}
<div class="flex flex-col gap-4 card bg-surface-50-900-token shadow p-4">
<div class="flex flex-col gap-2">
<div class="h4">{metadata.displayName}</div>
<div class="italic text-sm text-surface-500">{metadata.description}</div>
</div>
<div class="flex items-center gap-4">
<ProgressRadial
value={utilization}
meter="stroke-primary-500 dark:stroke-primary-50"
track="stroke-primary-500/20"
stroke={48}
font={112}
>
{Math.round(utilization)}%
</ProgressRadial>

<div class="grid grid-cols-[auto_auto] gap-x-4">
<div class="font-bold">Total</div>
<div>{quota.quantity}</div>
<div class="font-bold">Free</div>
<div>{data.quotas.free[i].quantity}</div>
<div class="contents text-surface-400-500-token">
<div class="font-bold">Committed</div>
<div>{data.quotas.allocated[i].committed}</div>
<div class="font-bold">Reserved</div>
<div>{data.quotas.allocated[i].reserved}</div>
<div class="grid grid-cols-[auto_auto] gap-x-4">
<div class="font-bold">Total</div>
<div>{quota.quantity}</div>
<div class="font-bold">Used</div>
<div>{quota.quantity - data.quotas.free[i].quantity}</div>
<div class="font-bold">Free</div>
<div>{data.quotas.free[i].quantity}</div>
<div class="contents text-surface-400-500-token">
<div class="font-bold">Committed</div>
<div>{data.quotas.allocated[i].committed}</div>
<div class="font-bold">Reserved</div>
<div>{data.quotas.allocated[i].reserved}</div>
</div>
</div>
</div>
</div>
</div>
{/if}
{/each}
</div>
</ShellSection>
Expand Down
3 changes: 3 additions & 0 deletions src/routes/(shell)/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import * as Clients from '$lib/clients';
export const load: PageLoad = async ({ fetch, parent }) => {
const { token, organizationID } = await parent();

const quotaMetadata = Clients.identity(token, fetch).apiV1QuotasGet();

const quotas = Clients.identity(token, fetch).apiV1OrganizationsOrganizationIDQuotasGet({
organizationID: organizationID
});

return {
quotaMetadata: await quotaMetadata,
quotas: await quotas
};
};
35 changes: 9 additions & 26 deletions src/routes/(shell)/identity/quotas/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,37 +36,20 @@
.apiV1OrganizationsOrganizationIDQuotasPut(parameters)
.catch((e: Error) => Clients.error(toastStore, e));
}
type QuotaMetadata = {
title: string;
description: string;
};
const metadata: Record<string, QuotaMetadata> = {
clusters: {
title: 'Clusters',
description: 'All cluster types e.g. Kubernetes, compute, etc.'
},
servers: {
title: 'Servers',
description: 'All servers and virtual machines.'
},
gpus: {
title: 'GPUs',
description: 'General purpose GPUs.'
}
};
</script>

<ShellPage {settings}>
<ShellSection title="Quotas">
{#each quotas.capacity as quota, i}
<NumberInput
id={quota.kind}
label={metadata[quota.kind].title}
hint={metadata[quota.kind].description}
bind:value={quotas.capacity[i].quantity}
/>
{@const metadata = data.quotaMetadata.find((x) => x.name == quota.kind)}
{#if metadata}
<NumberInput
id={quota.kind}
label={metadata.displayName}
hint={metadata.description}
bind:value={quotas.capacity[i].quantity}
/>
{/if}
{/each}
</ShellSection>

Expand Down
3 changes: 3 additions & 0 deletions src/routes/(shell)/identity/quotas/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import * as Clients from '$lib/clients';
export const load: PageLoad = async ({ fetch, parent }) => {
const { token, organizationID } = await parent();

const quotaMetadata = Clients.identity(token, fetch).apiV1QuotasGet();

const quotas = Clients.identity(token, fetch).apiV1OrganizationsOrganizationIDQuotasGet({
organizationID: organizationID
});

return {
quotaMetadata: await quotaMetadata,
quotas: await quotas
};
};

0 comments on commit e89dcc8

Please sign in to comment.