Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion projects/lib/utils/utils/resource-field-by-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ export const getResourceValueByJsonPath = (
return undefined;
}

const value = jsonpath.query(resource || {}, `$.${property}`);
const value = jsonpath.query(resource, `$.${property}`);
return value.length ? value[0] : undefined;
};
1 change: 1 addition & 0 deletions projects/lib/utils/utils/type-helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type RequireSome<T, K extends keyof T> = Required<Pick<T, K>> & Partial<Omit<T, K>>;
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { DynamicSelectComponent } from './dynamic-select.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ResourceService } from '@platform-mesh/portal-ui-lib/services';
import { of } from 'rxjs';
import { DynamicSelectComponent } from './dynamic-select.component';


const mockResourceService = {
list: jest.fn(),
Expand Down Expand Up @@ -39,7 +40,7 @@ describe('DynamicSelectComponent', () => {

const context: any = { id: 'ctx' };

component.field = (() => fieldDefinition) as any;
component.dynamicValuesDefinition = (() => fieldDefinition) as any;
component.context = (() => context) as any;
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
import {
Component,
DestroyRef,
effect,
inject,
input,
output,
signal,
} from '@angular/core';
import { Component, DestroyRef, effect, inject, input, output, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
FieldDefinition,
} from '@openmfp/portal-ui-lib';
import { FieldDefinition } from '@openmfp/portal-ui-lib';
import { ResourceNodeContext, ResourceService } from '@platform-mesh/portal-ui-lib/services';
import { getValueByPath } from '@platform-mesh/portal-ui-lib/utils';
import { RequireSome } from '@platform-mesh/portal-ui-lib/utils/utils/type-helpers';
import { OptionComponent, SelectComponent } from '@ui5/webcomponents-ngx';
import { Observable, map } from 'rxjs';


@Component({
selector: 'dynamic-select',
imports: [SelectComponent, OptionComponent],
templateUrl: './dynamic-select.component.html',
styleUrl: './dynamic-select.component.scss',
})
export class DynamicSelectComponent {
field = input.required<FieldDefinition>();
dynamicValuesDefinition = input.required<NonNullable<FieldDefinition['dynamicValuesDefinition']>>();
context = input.required<ResourceNodeContext>();

value = input<string>();
value = input<string>('');
required = input<boolean>(false);
valueState = input<
'None' | 'Positive' | 'Critical' | 'Negative' | 'Information'
Expand All @@ -43,7 +35,7 @@ export class DynamicSelectComponent {

constructor() {
effect(() => {
this.getDynamicValues(this.field(), this.context())
this.getDynamicValues(this.dynamicValuesDefinition(), this.context())
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((result) => {
this.dynamicValues$.set(result);
Expand All @@ -52,26 +44,22 @@ export class DynamicSelectComponent {
}

private getDynamicValues(
fieldDefinition: FieldDefinition,
dynamicValuesDefinition: NonNullable<
FieldDefinition['dynamicValuesDefinition']
>,
context: ResourceNodeContext,
): Observable<{ value: string; key: string }[]> {
return this.resourceService
.list(
fieldDefinition.dynamicValuesDefinition.operation,
fieldDefinition.dynamicValuesDefinition.gqlQuery,
dynamicValuesDefinition.operation,
dynamicValuesDefinition.gqlQuery,
context,
)
.pipe(
map((result) =>
result.map((resource) => ({
value: getValueByPath(
resource,
fieldDefinition.dynamicValuesDefinition.value,
),
key: getValueByPath(
resource,
fieldDefinition.dynamicValuesDefinition.key,
),
value: getValueByPath(resource, dynamicValuesDefinition.value),
key: getValueByPath(resource, dynamicValuesDefinition.key),
})),
),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</div>
</ui5-title>
<ui5-text class="resource-title-subheading" slot="subheading">
The {{ resourceDefinition().singular }} for
The {{ resourceDefinition()?.singular }} for
{{ resource()?.spec?.displayName || resourceId() }}
</ui5-text>

Expand All @@ -28,40 +28,41 @@

<ui5-dynamic-page-header slot="headerArea">
<div class="resource-info">
@if (resourceDefinition().ui?.logoUrl) {
@if (resourceDefinition()?.ui?.logoUrl) {
<img
class="resource-logo"
src="{{ resourceDefinition().ui.logoUrl }}"
src="{{ resourceDefinition()?.ui?.logoUrl }}"
alt="Logo"
/>
}
<div class="resource-info-cell">
<ui5-label>Workspace Path</ui5-label>
<p>{{ workspacePath() }}</p>
</div>

@for (field of viewFields(); track field.property) {
<div class="resource-info-cell">
@if(field.group) {
<ui5-label>{{ field.group.label ?? field.group.name }}</ui5-label>
<p>
@for (field of field.group.fields; let last = $last; track field.label) {
<span [class.multiline]="field.group.multiline ?? true">
<span>{{ field.label }}: </span>
<value-cell [labelDisplay]="field.labelDisplay" [value]="getResourceValueByJsonPath(resource(), field)"></value-cell>
</span>
@if (!last && field.group.delimiter) {
<span>{{ field.group.delimiter }}</span>
@if (resource(); as resource) {
@for (viewField of viewFields(); track viewField.property) {
<div class="resource-info-cell">
@if(viewField.group) {
<ui5-label>{{ viewField.group.label ?? viewField.group.name }}</ui5-label>
<p>
@for (field of viewField.group.fields; let last = $last; track field.label) {
<span [class.multiline]="viewField.group.multiline ?? true">
<span>{{ field.label }}: </span>
<value-cell [labelDisplay]="field.labelDisplay" [value]="getResourceValueByJsonPath(resource, field)"></value-cell>
</span>
@if (!last && viewField.group.delimiter) {
<span>{{ viewField.group.delimiter }}</span>
}
}
}
</p>
} @else {
<ui5-label>{{ field.label }}</ui5-label>
<p>
<value-cell [labelDisplay]="field.labelDisplay" [value]="getResourceValueByJsonPath(resource(), field)"></value-cell>
</p>
}
</div>
</p>
} @else {
<ui5-label>{{ viewField.label }}</ui5-label>
<p>
<value-cell [labelDisplay]="viewField.labelDisplay" [value]="getResourceValueByJsonPath(resource, viewField)"></value-cell>
</p>
}
</div>
}
}
</div>
</ui5-dynamic-page-header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ describe('DetailViewComponent', () => {

afterEach(() => {
jest.restoreAllMocks();
delete global.URL.createObjectURL;
delete (global as any).URL.createObjectURL;
});

it('should create the component', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import { processFields } from '../../../utils/proccess-fields';
import { ValueCellComponent } from '../value-cell/value-cell.component';
import { kubeConfigTemplate } from './kubeconfig-template';
import {
ChangeDetectionStrategy,
Component,
Expand Down Expand Up @@ -34,6 +31,10 @@ import {
ToolbarButtonComponent,
ToolbarComponent,
} from '@ui5/webcomponents-ngx';
import { processFields } from '../../../utils/proccess-fields';
import { ValueCellComponent } from '../value-cell/value-cell.component';
import { kubeConfigTemplate } from './kubeconfig-template';
import { validateKubeconfigProps } from '../../../utils/ts-guargs/validate-kubeconfig-props';

const defaultFields: FieldDefinition[] = [
{
Expand Down Expand Up @@ -68,15 +69,15 @@ export class DetailViewComponent {
private envConfigService = inject(EnvConfigService);
protected readonly getResourceValueByJsonPath = getResourceValueByJsonPath;

LuigiClient = input<LuigiClient>();
context = input<ResourceNodeContext>();
resource = signal<Resource | null>(null);
LuigiClient = input.required<LuigiClient>();
context = input.required<ResourceNodeContext>();
resource = signal<Resource | undefined>(undefined);

resourceDefinition = computed(() => this.context().resourceDefinition);
resourceFields = computed(
() => this.resourceDefinition().ui?.detailView?.fields || defaultFields,
() => this.resourceDefinition()?.ui?.detailView?.fields || defaultFields,
);
resourceId = computed(() => this.context().entity.metadata.name);
resourceId = computed(() => this.context().entity?.metadata.name);
workspacePath = computed(() =>
this.gatewayService.resolveKcpPath(this.context()),
);
Expand All @@ -89,15 +90,26 @@ export class DetailViewComponent {
}

private readResource(): void {
const resourceDefinition = this.getResourceDefinition();
const fields = generateGraphQLFields(this.resourceFields());
const queryOperation = replaceDotsAndHyphensWithUnderscores(
this.resourceDefinition().group,
resourceDefinition.group,
);
const kind = this.resourceDefinition().kind;
const kind = resourceDefinition.kind;

const resourceId = this.resourceId();
if (!resourceId) {
this.LuigiClient().uxManager().showAlert({
text: 'Resource ID is not defined',
type: 'error',
});

throw new Error('Resource ID is not defined');
}

this.resourceService
.read(
this.resourceId(),
resourceId,
queryOperation,
kind,
fields,
Expand All @@ -110,25 +122,54 @@ export class DetailViewComponent {
}

navigateToParent() {
const parentNavigationContext =
this.context().parentNavigationContexts?.at(-1);
if (!parentNavigationContext) {
this.LuigiClient().uxManager().showAlert({
text: 'Parent navigation context is not defined',
type: 'error',
});

throw new Error('Parent navigation context is not defined');
}

this.LuigiClient()
.linkManager()
.fromContext(this.context().parentNavigationContexts.at(-1))
.fromContext(parentNavigationContext)
.navigate('/');
}

async downloadKubeConfig() {
const { oidcIssuerUrl } = await this.envConfigService.getEnvConfig();
const kubeconfigProps = {
accountId: this.context().accountId,
organization: this.context().organization,
kcpCA: this.context().kcpCA,
token: this.context().token,
kcpWorkspaceUrl: this.context().portalContext.kcpWorkspaceUrl,
};

try {
validateKubeconfigProps(kubeconfigProps);
} catch (error) {
this.LuigiClient().uxManager().showAlert({
text: error.message,
type: 'error',
});

throw error;
}

const kubeConfig = kubeConfigTemplate
.replaceAll('<cluster-name>', this.context().accountId)
.replaceAll('<org-name>', this.context().organization)
.replaceAll('<cluster-name>', kubeconfigProps.accountId)
.replaceAll('<org-name>', kubeconfigProps.organization)
.replaceAll(
'<server-url>',
`${this.context().portalContext.kcpWorkspaceUrl}:${this.context().accountId}`,
`${kubeconfigProps.kcpWorkspaceUrl}:${kubeconfigProps.accountId}`,
)
.replaceAll('<oidc-issuer-url>', oidcIssuerUrl)
.replaceAll('<ca-data>', this.context().kcpCA)
.replaceAll('<token>', this.context().token);
.replaceAll('<ca-data>', kubeconfigProps.kcpCA)
.replaceAll('<token>', kubeconfigProps.token);

const blob = new Blob([kubeConfig], { type: 'application/plain' });
const url = URL.createObjectURL(blob);
Expand All @@ -138,4 +179,18 @@ export class DetailViewComponent {
a.download = 'kubeconfig.yaml';
a.click();
}

private getResourceDefinition() {
const resourceDefinition = this.resourceDefinition();
if (!resourceDefinition) {
this.LuigiClient().uxManager().showAlert({
text: 'Resource definition is not defined',
type: 'error',
});

throw new Error('Resource definition is not defined');
}

return resourceDefinition;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
[valueState]="getValueState(fieldProperty)"
[disabled]="isEditMode() && isCreateFieldOnly(field)"
>
@for (value of [''].concat(field.values); track value) {
@for (value of [''].concat(field.values ?? []); track value) {
<ui5-option
[value]="value"
[selected]="value === form.controls[fieldProperty].value"
Expand All @@ -37,12 +37,12 @@
} @else if (field.dynamicValuesDefinition) {
<dynamic-select
[context]="context()"
[field]="field"
[dynamicValuesDefinition]="field.dynamicValuesDefinition"
[value]="form.controls[fieldProperty].value"
(input)="setFormControlValue($event, fieldProperty)"
(change)="setFormControlValue($event, fieldProperty)"
(blur)="onFieldBlur(fieldProperty)"
[required]="field.required"
[required]="!!field.required"
[valueState]="getValueState(fieldProperty)" />
} @else {
<ui5-input
Expand Down
Loading
Loading