diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 0c1c0f391..5adebd244 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -57,6 +57,7 @@ const INLINE_NON_VOID_ELEMENTS = [ ignorePatterns: [ 'node_modules', 'dist', + 'src/types/auto-generated.d.ts', ], plugins: ['vue', 'import', '@typescript-eslint'], extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', 'standard', '@vue/typescript', 'plugin:import/recommended', 'plugin:import/typescript'], diff --git a/mk/build.mk b/mk/build.mk index 70e066451..233016a40 100644 --- a/mk/build.mk +++ b/mk/build.mk @@ -39,3 +39,7 @@ deploy/preview: @$(MAKE) build/preview @$(MAKE) deploy/test +build/types: + @npx openapi-typescript \ + ../kuma/docs/generated/openapi.yaml \ + -o src/types/auto-generated.d.ts diff --git a/package-lock.json b/package-lock.json index 219344fed..eaaa62e5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "deepmerge": "^4.3.1", "js-yaml": "^4.1.0", "object.groupby": "^1.0.3", + "openapi-fetch": "^0.9.8", "path-to-regexp": "^7.0.0", "pretty-bytes": "^6.1.1", "prismjs": "^1.29.0", @@ -63,6 +64,7 @@ "lockfile-lint": "^4.14.0", "marked": "^13.0.1", "msw": "^2.3.1", + "openapi-typescript": "^6.7.6", "postcss": "^8.4.39", "postcss-html": "^1.7.0", "sass": "^1.77.6", @@ -2811,6 +2813,15 @@ "npm": ">=6.14.13" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz", @@ -11542,6 +11553,66 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openapi-fetch": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.9.8.tgz", + "integrity": "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg==", + "dependencies": { + "openapi-typescript-helpers": "^0.0.8" + } + }, + "node_modules/openapi-typescript": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-6.7.6.tgz", + "integrity": "sha512-c/hfooPx+RBIOPM09GSxABOZhYPblDoyaGhqBkD/59vtpN21jEuWKDlM0KYTvqJVlSYjKs0tBcIdeXKChlSPtw==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "fast-glob": "^3.3.2", + "js-yaml": "^4.1.0", + "supports-color": "^9.4.0", + "undici": "^5.28.4", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + } + }, + "node_modules/openapi-typescript-helpers": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.8.tgz", + "integrity": "sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g==" + }, + "node_modules/openapi-typescript/node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/openapi-typescript/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -14478,6 +14549,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/package.json b/package.json index 20e0de301..20472b385 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "deepmerge": "^4.3.1", "js-yaml": "^4.1.0", "object.groupby": "^1.0.3", + "openapi-fetch": "^0.9.8", "path-to-regexp": "^7.0.0", "pretty-bytes": "^6.1.1", "prismjs": "^1.29.0", @@ -71,6 +72,7 @@ "marked": "^13.0.1", "msw": "^2.3.1", "postcss": "^8.4.39", + "openapi-typescript": "^6.7.6", "postcss-html": "^1.7.0", "sass": "^1.77.6", "shiki": "^1.10.3", diff --git a/src/app/application/index.ts b/src/app/application/index.ts index 9863835ad..e830a9d2b 100644 --- a/src/app/application/index.ts +++ b/src/app/application/index.ts @@ -24,6 +24,7 @@ import type { ServiceDefinition } from '@/services/utils' import { token, createInjections, constant } from '@/services/utils' import type { Component } from 'vue' export * from './services/can' +export { defineSources } from './services/data-source' // temporary simple "JSON data only" structuredClone polyfill for cloning JSON // data @@ -61,6 +62,7 @@ const $ = { env: token('application.env'), EnvVars: token('EnvVars'), + fetch: token('application.fetch'), can: token('application.can'), features: token('application.can.features'), @@ -124,6 +126,10 @@ export const services = (app: Record): ServiceDefinition[] => { ], }], + [$.fetch, { + service: () => fetch, + }], + [$.can, { service: can, arguments: [ diff --git a/src/app/common/code-block/ResourceCodeBlock.vue b/src/app/common/code-block/ResourceCodeBlock.vue index 326846551..0b4760654 100644 --- a/src/app/common/code-block/ResourceCodeBlock.vue +++ b/src/app/common/code-block/ResourceCodeBlock.vue @@ -36,7 +36,7 @@ if (expanded) { toggle() } - cb((text: Entity) => copy(toYamlRepresentation(text)), (e: unknown) => console.error(e)) + cb((text: Object) => copy(toYamlRepresentation(text)), (e: unknown) => console.error(e)) }" :copying="expanded" /> @@ -51,17 +51,16 @@ import { computed } from 'vue' import CodeBlock from './CodeBlock.vue' -import type { Entity } from '@/types/index.d' import { useI18n } from '@/utilities' import { toYaml } from '@/utilities/toYaml' -type Resolve = (data: Entity) => void +type Resolve = (data: Object) => void type CopyCallback = (resolve: Resolve, reject: (e: unknown) => void) => void const { t } = useI18n() const props = withDefaults(defineProps<{ - resource: Entity + resource: Object codeMaxHeight?: string isSearchable?: boolean query?: string @@ -82,8 +81,13 @@ const emit = defineEmits<{ }>() const yamlUniversal = computed(() => toYamlRepresentation(props.resource)) -function toYamlRepresentation(resource: Entity): string { - const { creationTime, modificationTime, ...resourceWithoutTimes } = resource - return toYaml(resourceWithoutTimes) +function toYamlRepresentation(resource: Object): string { + if ('creationTime' in resource) { + delete resource.creationTime + } + if ('modificationTime' in resource) { + delete resource.modificationTime + } + return toYaml(resource) } diff --git a/src/app/kuma/services/kuma-api/Api.ts b/src/app/kuma/services/kuma-api/Api.ts index 7c9b0e4ce..4896e221d 100644 --- a/src/app/kuma/services/kuma-api/Api.ts +++ b/src/app/kuma/services/kuma-api/Api.ts @@ -3,7 +3,7 @@ import type Env from '@/app/application/services/env/Env' export class Api { constructor( - protected client: RestClient, + public client: RestClient, protected env: Env['var'], ) { } diff --git a/src/app/services/data/MeshExternalService.ts b/src/app/services/data/MeshExternalService.ts new file mode 100644 index 000000000..c8a4315c8 --- /dev/null +++ b/src/app/services/data/MeshExternalService.ts @@ -0,0 +1,36 @@ +import type { components } from '@/types/auto-generated.d' +type GeneratedMeshExternalService = components['schemas']['MeshExternalServiceItem'] +type GeneratedMeshExternalServiceList = components['responses']['MeshExternalServiceList']['content']['application/json'] + +export const MeshExternalService = { + fromObject(item: GeneratedMeshExternalService) { + const labels = item.labels ?? {} + const name = labels['kuma.io/display-name'] ?? item.name + const namespace = labels['k8s.kuma.io/namespace'] ?? '' + return { + ...item, + config: item, + id: item.name, + name, + namespace, + labels, + status: ((item = {}) => { + return { + ...item, + addresses: Array.isArray(item.addresses) ? item.addresses : [], + } + })(item.status), + + } + }, + + fromCollection(collection: GeneratedMeshExternalServiceList) { + const items = Array.isArray(collection.items) ? collection.items.map(MeshExternalService.fromObject) : [] + return { + ...collection, + items, + total: collection.total ?? items.length, + } + }, +} +export type MeshExternalService = ReturnType diff --git a/src/app/services/data/MeshService.ts b/src/app/services/data/MeshService.ts new file mode 100644 index 000000000..768280d8a --- /dev/null +++ b/src/app/services/data/MeshService.ts @@ -0,0 +1,46 @@ +import type { components } from '@/types/auto-generated.d' +type GeneratedMeshService = components['schemas']['MeshServiceItem'] +type GeneratedMeshServiceList = components['responses']['MeshServiceList']['content']['application/json'] + +export const MeshService = { + fromObject(item: GeneratedMeshService) { + const labels = item.labels ?? {} + const name = labels['kuma.io/display-name'] ?? item.name + const namespace = labels['k8s.kuma.io/namespace'] ?? '' + return { + ...item, + id: item.name, + name, + namespace, + labels, + spec: ((item = {}) => { + return { + ports: Array.isArray(item.ports) ? item.ports : [], + selector: ((item = {}) => { + return { + dataplaneTags: Object.keys(item.dataplaneTags ?? {}).length > 0 ? item.dataplaneTags! : {}, + } + })(item.selector), + } + })(item.spec), + status: ((item = {}) => { + return { + tls: typeof item.tls !== 'undefined' ? item.tls : { status: 'NotReady' }, + vips: Array.isArray(item.vips) ? item.vips : [], + addresses: Array.isArray(item.addresses) ? item.addresses : [], + } + })(item.status), + config: item, + } + }, + + fromCollection(collection: GeneratedMeshServiceList) { + const items = Array.isArray(collection.items) ? collection.items.map(MeshService.fromObject) : [] + return { + ...collection, + items, + total: collection.total ?? items.length, + } + }, +} +export type MeshService = ReturnType diff --git a/src/app/services/data/index.ts b/src/app/services/data/index.ts index e3935afff..046b3a4dc 100644 --- a/src/app/services/data/index.ts +++ b/src/app/services/data/index.ts @@ -1,42 +1,15 @@ import type { PaginatedApiListResponse } from '@/types/api.d' import type { - MeshService as PartialMeshService, - MeshExternalService as PartialMeshExternalService, ExternalService as PartialExternalService, ServiceInsight as PartialServiceInsight, ServiceStatus as ServiceTypeCount, } from '@/types/index.d' +export * from './MeshService' +export * from './MeshExternalService' export type ExternalService = PartialExternalService & { config: PartialExternalService } -export type MeshService = Omit & { - id: string - namespace: string - labels: NonNullable - spec: { - ports: NonNullable - selector: { - dataplaneTags: NonNullable['dataplaneTags']> - } - } - status: { - addresses: NonNullable - vips: NonNullable - tls: NonNullable - } - config: PartialMeshService -} -export type MeshExternalService = Omit & { - id: string - namespace: string - labels: NonNullable - config: PartialMeshExternalService - status: { - addresses: NonNullable - vip?: PartialMeshExternalService['status']['vip'] - } -} export type ServiceInsight = PartialServiceInsight & { serviceType: 'internal' | 'external' | 'gateway_builtin' | 'gateway_delegated' @@ -73,78 +46,6 @@ export const ServiceInsight = { } }, } -export const MeshService = { - fromObject(item: PartialMeshService): MeshService { - const labels = item.labels ?? {} - const name = labels['kuma.io/display-name'] ?? item.name - const namespace = labels['k8s.kuma.io/namespace'] ?? '' - return { - ...item, - config: item, - id: item.name, - name, - namespace, - labels, - spec: ((item = {}) => { - return { - ports: Array.isArray(item.ports) ? item.ports : [], - selector: ((item = {}) => { - return { - dataplaneTags: Object.keys(item.dataplaneTags ?? {}).length > 0 ? item.dataplaneTags! : {}, - } - })(item.selector), - } - })(item.spec), - status: ((item = {}) => { - return { - tls: typeof item.tls !== 'undefined' ? item.tls : { status: '' }, - vips: Array.isArray(item.vips) ? item.vips : [], - addresses: Array.isArray(item.addresses) ? item.addresses : [], - } - })(item.status), - } - }, - - fromCollection(collection: PaginatedApiListResponse): PaginatedApiListResponse { - const items = Array.isArray(collection.items) ? collection.items.map(MeshService.fromObject) : [] - return { - ...collection, - items, - total: collection.total ?? items.length, - } - }, -} -export const MeshExternalService = { - fromObject(item: PartialMeshExternalService): MeshExternalService { - const labels = item.labels ?? {} - const name = labels['kuma.io/display-name'] ?? item.name - const namespace = labels['k8s.kuma.io/namespace'] ?? '' - return { - ...item, - config: item, - id: item.name, - name, - namespace, - labels, - status: ((item = {}) => { - return { - ...item, - addresses: Array.isArray(item.addresses) ? item.addresses : [], - } - })(item.status), - - } - }, - - fromCollection(collection: PaginatedApiListResponse): PaginatedApiListResponse { - const items = Array.isArray(collection.items) ? collection.items.map(MeshExternalService.fromObject) : [] - return { - ...collection, - items, - total: collection.total ?? items.length, - } - }, -} export function getServiceTypeCount({ total = 0, internal = 0, external = 0 }: ServiceTypeCount): Required { return { diff --git a/src/app/services/sources.ts b/src/app/services/sources.ts index 68250ea53..771e9614f 100644 --- a/src/app/services/sources.ts +++ b/src/app/services/sources.ts @@ -1,8 +1,11 @@ +import createClient from 'openapi-fetch' + import { MeshService, MeshExternalService, ExternalService, ServiceInsight } from './data' import type { DataSourceResponse } from '@/app/application' import { defineSources } from '@/app/application/services/data-source' import type KumaApi from '@/app/kuma/services/kuma-api/KumaApi' import type { PaginatedApiListResponse as CollectionResponse, ServiceInsightsParameters } from '@/types/api.d' +import type { paths } from '@/types/auto-generated.d' export type { ServiceInsight } from './data' @@ -13,47 +16,108 @@ export type ServiceInsightCollectionSource = DataSourceResponse export const sources = (api: KumaApi) => { + const http = createClient({ + baseUrl: '', + fetch: api.client.fetch, + }) return defineSources({ '/meshes/:mesh/mesh-services': async (params) => { const { mesh, size } = params const offset = params.size * (params.page - 1) - return MeshService.fromCollection(await api.getAllMeshServicesFromMesh({ mesh }, { size, offset })) + const res = await http.GET('/meshes/{mesh}/meshservices', { + params: { + path: { + mesh, + }, + query: { + offset, + size, + }, + }, + }) + + return MeshService.fromCollection(res.data!) }, '/meshes/:mesh/mesh-service/:name': async (params) => { const { mesh, name } = params - return MeshService.fromObject(await api.getMeshService({ mesh, name })) + const res = await http.GET('/meshes/{mesh}/meshservices/{name}', { + params: { + path: { + mesh, + name, + }, + }, + }) + return MeshService.fromObject(res.data!) }, '/meshes/:mesh/mesh-service/:name/as/kubernetes': async (params) => { const { mesh, name } = params - - return api.getMeshService({ mesh, name }, { - format: 'kubernetes', + const res = await http.GET('/meshes/{mesh}/meshservices/{name}', { + params: { + path: { + mesh, + name, + }, + query: { + format: 'kubernetes', + }, + }, }) + return res.data! }, '/meshes/:mesh/mesh-external-services': async (params) => { const { mesh, size } = params const offset = params.size * (params.page - 1) - return MeshExternalService.fromCollection(await api.getAllMeshExternalServicesFromMesh({ mesh }, { size, offset })) + const res = await http.GET('/meshes/{mesh}/meshexternalservices', { + params: { + path: { + mesh, + }, + query: { + offset, + size, + }, + }, + }) + return MeshExternalService.fromCollection(res.data!) }, '/meshes/:mesh/mesh-external-service/:name': async (params) => { const { mesh, name } = params - return MeshExternalService.fromObject(await api.getMeshExternalService({ mesh, name })) + const res = await http.GET('/meshes/{mesh}/meshexternalservices/{name}', { + params: { + path: { + mesh, + name, + }, + }, + }) + + return MeshExternalService.fromObject(res.data!) }, '/meshes/:mesh/mesh-external-service/:name/as/kubernetes': async (params) => { const { mesh, name } = params - return api.getMeshExternalService({ mesh, name }, { - format: 'kubernetes', + const res = await http.GET('/meshes/{mesh}/meshexternalservices/{name}', { + params: { + path: { + mesh, + name, + }, + query: { + format: 'kubernetes', + }, + }, }) + return res.data! }, '/meshes/:mesh/service-insights/of/:serviceType': async (params) => { diff --git a/src/app/services/views/MeshExternalServiceSummaryView.vue b/src/app/services/views/MeshExternalServiceSummaryView.vue index 613994c27..e83f3a938 100644 --- a/src/app/services/views/MeshExternalServiceSummaryView.vue +++ b/src/app/services/views/MeshExternalServiceSummaryView.vue @@ -145,7 +145,7 @@