Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions projects/lib/utils/utils/resource-field-by-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Resource } from '@platform-mesh/portal-ui-lib/models';
import jsonpath from 'jsonpath';

export const getResourceValueByJsonPath = (
resource: Resource,
resource: Resource | null,
field: { jsonPathExpression?: string; property?: string | string[] },
) => {
const property = field?.jsonPathExpression || field?.property;
Expand All @@ -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;
};
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,10 +28,10 @@

<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"
/>
}
Expand All @@ -40,25 +40,25 @@
<p>{{ workspacePath() }}</p>
</div>

@for (field of viewFields(); track field.property) {
@for (viewField of viewFields(); track viewField.property) {
<div class="resource-info-cell">
@if(field.group) {
<ui5-label>{{ field.group.label ?? field.group.name }}</ui5-label>
@if(viewField.group) {
<ui5-label>{{ viewField.group.label ?? viewField.group.name }}</ui5-label>
<p>
@for (field of field.group.fields; let last = $last; track field.label) {
<span [class.multiline]="field.group.multiline ?? true">
@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 && field.group.delimiter) {
<span>{{ field.group.delimiter }}</span>
@if (!last && viewField.group.delimiter) {
<span>{{ viewField.group.delimiter }}</span>
}
}
</p>
} @else {
<ui5-label>{{ field.label }}</ui5-label>
<ui5-label>{{ viewField.label }}</ui5-label>
<p>
<value-cell [labelDisplay]="field.labelDisplay" [value]="getResourceValueByJsonPath(resource(), field)"></value-cell>
<value-cell [labelDisplay]="viewField.labelDisplay" [value]="getResourceValueByJsonPath(resource(), viewField)"></value-cell>
</p>
}
</div>
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,9 @@ 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';

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

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

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 +89,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 @@ -106,29 +117,64 @@ export class DetailViewComponent {
)
.subscribe({
next: (result) => this.resource.set(result),
error: (error) => {
this.LuigiClient().uxManager().showAlert({
text: `Failed to read resource: ${error.message}`,
type: 'error',
});
},
});
}

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 +184,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 @@ -42,7 +42,7 @@
(input)="setFormControlValue($event, fieldProperty)"
(change)="setFormControlValue($event, fieldProperty)"
(blur)="onFieldBlur(fieldProperty)"
[required]="field.required"
[required]="field.required ?? false"
[valueState]="getValueState(fieldProperty)" />
} @else {
<ui5-input
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
FormControl,
FormGroup,
ReactiveFormsModule,
ValidatorFn,
Validators,
} from '@angular/forms';
import { FieldDefinition, Resource } from '@openmfp/portal-ui-lib';
Expand Down Expand Up @@ -57,7 +58,7 @@ import { set } from 'lodash';
})
export class CreateResourceModalComponent implements OnInit {
fields = input<FieldDefinition[]>([]);
context = input<ResourceNodeContext>();
context = input.required<ResourceNodeContext>();
resource = output<Resource>();
updateResource = output<Resource>();
dialog = viewChild<DialogComponent>('dialog');
Expand All @@ -74,7 +75,7 @@ export class CreateResourceModalComponent implements OnInit {
}

open(resource?: Resource) {
this.originalResource.set(resource);
this.originalResource.set(resource ?? null);
this.form = this.fb.group(this.createControls(resource));
const dialog = this.dialog();
if (dialog) {
Expand Down Expand Up @@ -159,7 +160,8 @@ export class CreateResourceModalComponent implements OnInit {
}

private getValidator(fieldDefinition: FieldDefinition) {
const validators = [];
const validators: ValidatorFn[] = [];

if (fieldDefinition.required) {
validators.push(Validators.required);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
<ui5-title slot="heading">
<div class="title-logo">
<div>{{ heading() }}</div>
@if (resourceDefinition().ui?.logoUrl) {
@if (resourceDefinition()?.ui?.logoUrl) {
<img
class="logo"
src="{{ resourceDefinition().ui.logoUrl }}"
src="{{ resourceDefinition()?.ui?.logoUrl }}"
alt="Logo"
/>
}
</div>
</ui5-title>
<ui5-text class="title-subheading" slot="subheading">
This page displays the created
{{ resourceDefinition().plural }} in your environment
{{ resourceDefinition()?.plural }} in your environment
</ui5-text>

<ui5-toolbar
Expand Down Expand Up @@ -92,7 +92,7 @@
@if (hasUiCreateViewFields()) {
<create-resource-modal
#createModal
[fields]="resourceDefinition().ui?.createView?.fields"
[fields]="resourceDefinition()?.ui?.createView?.fields ?? []"
(resource)="create($event)"
(updateResource)="update($event)"
[context]="context()"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,17 @@ describe('ListViewComponent', () => {
});

it('should check create view fields existence', () => {
component.resourceDefinition().ui.createView = {
fields: [{ property: 'any' }],
};
fixture.componentRef.setInput('context', {
resourceDefinition: {
ui: {
createView: {
fields: [{ property: 'any' }],
},
},
},
});
fixture.detectChanges();

expect(component.hasUiCreateViewFields()).toBe(true);
});

Expand Down
Loading
Loading