Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
198 changes: 143 additions & 55 deletions package-lock.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,8 @@ describe('NodeContextProcessingServiceImpl', () => {

expect(ctx.entity).toBe(mockEntity);
expect(ctx.entityId).toBe('cluster1/test-entity');
expect(entityNode.context.entity).toBe(mockEntity);
expect(entityNode.context.entityId).toBe('cluster1/test-entity');
expect(entityNode.context!.entity).toBe(mockEntity);
expect(entityNode.context!.entityId).toBe('cluster1/test-entity');
});

it('should handle entity without kcp.io/cluster annotation', async () => {
Expand Down Expand Up @@ -356,8 +356,8 @@ describe('NodeContextProcessingServiceImpl', () => {

expect(ctx.entity).toBe(mockEntity);
expect(ctx.entityId).toBe('undefined/test-entity');
expect(entityNode.context.entity).toBe(mockEntity);
expect(entityNode.context.entityId).toBe('undefined/test-entity');
expect(entityNode.context!.entity).toBe(mockEntity);
expect(entityNode.context!.entityId).toBe('undefined/test-entity');
});

it('should log error and not update context when read fails', async () => {
Expand Down Expand Up @@ -388,7 +388,7 @@ describe('NodeContextProcessingServiceImpl', () => {
'Not able to read entity test-entity from test_group',
);
expect(ctx.entity).toBeUndefined();
expect(entityNode.context.entity).toBeUndefined();
expect(entityNode.context!.entity).toBeUndefined();

consoleErrorSpy.mockRestore();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { PortalNodeContext } from '../models/luigi-context';
import { PortalLuigiNode } from '../models/luigi-node';
import { CrdGatewayKcpPatchResolver } from './crd-gateway-kcp-patch-resolver.service';
import { Injectable, inject } from '@angular/core';
import {
NodeContextProcessingService,
} from '@openmfp/portal-ui-lib';
import { NodeContextProcessingService } from '@openmfp/portal-ui-lib';
import { ResourceService } from '@platform-mesh/portal-ui-lib/services';
import { replaceDotsAndHyphensWithUnderscores } from '@platform-mesh/portal-ui-lib/utils';
import { firstValueFrom } from 'rxjs';
import { PortalNodeContext } from '../models/luigi-context';
import { PortalLuigiNode } from '../models/luigi-node';
import { CrdGatewayKcpPatchResolver } from './crd-gateway-kcp-patch-resolver.service';


@Injectable({
providedIn: 'root',
Expand Down Expand Up @@ -70,8 +69,10 @@ export class NodeContextProcessingServiceImpl
ctx.entity = entity;
ctx.entityId = `${entity.metadata?.annotations?.['kcp.io/cluster']}/${entityId}`;
// update the node context of sa node to contain the entity for future context calculations
entityNode.context.entity = entity;
entityNode.context.entityId = ctx.entityId;
if (entityNode.context) {
entityNode.context.entity = entity;
entityNode.context.entityId = ctx.entityId;
}
} catch (e) {
console.error(`Not able to read entity ${entityId} from ${operation}`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ describe('CustomRoutingConfigServiceImpl', () => {
it('should redirect to error/404 when envConfig.baseDomain is undefined', async () => {
service['envConfig'] = {
...mockClientEnvironment,
baseDomain: undefined,
baseDomain: undefined as any,
};

Object.defineProperty(window, 'location', {
Expand Down
90 changes: 89 additions & 1 deletion projects/lib/services/resource/gateway.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GatewayService } from './gateway.service';
import { TestBed } from '@angular/core/testing';
import { LuigiCoreService } from '@openmfp/portal-ui-lib';
import { GatewayService } from './gateway.service';


describe('GatewayService', () => {
let service: GatewayService;
Expand All @@ -13,6 +14,7 @@ describe('GatewayService', () => {
crdGatewayApiUrl: 'https://example.com/:org1:acc1/graphql',
},
}),
showAlert: jest.fn(),
};

TestBed.configureTestingModule({
Expand Down Expand Up @@ -101,4 +103,90 @@ describe('GatewayService', () => {
expect(result).toBe(':org1:acc1');
});
});

describe('getCurrentKcpPath (via resolveKcpPath)', () => {
it('should extract kcp path from valid gateway URL', () => {
const nodeContext = {
portalContext: {
crdGatewayApiUrl: 'https://example.com/:org1:acc1/graphql',
},
token: 'token',
accountId: 'entityId',
};
const result = service.resolveKcpPath(nodeContext);
expect(result).toBe(':org1:acc1');
});

it('should extract kcp path with single segment', () => {
const nodeContext = {
portalContext: {
crdGatewayApiUrl: 'https://example.com/org1/graphql',
},
token: 'token',
accountId: 'entityId',
};
const result = service.resolveKcpPath(nodeContext);
expect(result).toBe('org1');
});

it('should show error alert and return empty string for invalid URL', () => {
const showAlertSpy = jest.spyOn(mockLuigiCoreService, 'showAlert');

const nodeContext = {
portalContext: {
crdGatewayApiUrl: 'https://example.com/invalid-url',
},
token: 'token',
accountId: 'entityId',
};

const result = service.resolveKcpPath(nodeContext);

expect(result).toBe('');
expect(showAlertSpy).toHaveBeenCalledWith({
text: 'Could not get current KCP path from gateway URL',
type: 'error',
});
});

it('should show error alert and return empty string for URL without /graphql suffix', () => {
const showAlertSpy = jest.spyOn(mockLuigiCoreService, 'showAlert');

const nodeContext = {
portalContext: {
crdGatewayApiUrl: 'https://example.com/:org1:acc1/api',
},
token: 'token',
accountId: 'entityId',
};

const result = service.resolveKcpPath(nodeContext);

expect(result).toBe('');
expect(showAlertSpy).toHaveBeenCalledWith({
text: 'Could not get current KCP path from gateway URL',
type: 'error',
});
});

it('should show error alert and return empty string for empty URL', () => {
const showAlertSpy = jest.spyOn(mockLuigiCoreService, 'showAlert');

const nodeContext = {
portalContext: {
crdGatewayApiUrl: '',
},
token: 'token',
accountId: 'entityId',
};

const result = service.resolveKcpPath(nodeContext);

expect(result).toBe('');
expect(showAlertSpy).toHaveBeenCalledWith({
text: 'Could not get current KCP path from gateway URL',
type: 'error',
});
});
});
});
42 changes: 29 additions & 13 deletions projects/lib/services/resource/gateway.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ResourceNodeContext } from './resource-node-context';
import { Injectable, inject } from '@angular/core';
import { LuigiCoreService } from '@openmfp/portal-ui-lib';
import { ResourceNodeContext } from './resource-node-context';


@Injectable({ providedIn: 'root' })
export class GatewayService {
Expand All @@ -11,8 +12,8 @@ export class GatewayService {
readFromParentKcpPath = false,
) {
const gatewayUrl = nodeContext.portalContext.crdGatewayApiUrl;
const kcpPathRegexp = /\/([^\/]+)\/graphql$/;
const currentKcpPath = gatewayUrl?.match(kcpPathRegexp)[1];
const currentKcpPath = this.getCurrentKcpPath(gatewayUrl);

return gatewayUrl?.replace(
currentKcpPath,
this.resolveKcpPath(nodeContext, readFromParentKcpPath),
Expand All @@ -31,19 +32,34 @@ export class GatewayService {
nodeContext: ResourceNodeContext,
readFromParentKcpPath = false,
) {
if (nodeContext.kcpPath) {
return nodeContext.kcpPath;
}

const gatewayUrl = nodeContext.portalContext.crdGatewayApiUrl;
const currentKcpPath = gatewayUrl?.match(/\/([^\/]+)\/graphql$/)[1];
const currentKcpPath = this.getCurrentKcpPath(gatewayUrl);
const lastIndex = currentKcpPath.lastIndexOf(':');

let kcpPath = currentKcpPath;
if (nodeContext.kcpPath) {
kcpPath = nodeContext.kcpPath;
} else if (readFromParentKcpPath) {
const lastIndex = currentKcpPath.lastIndexOf(':');
if (lastIndex !== -1) {
kcpPath = currentKcpPath.slice(0, lastIndex);
}
if (readFromParentKcpPath && lastIndex !== -1) {
return currentKcpPath.slice(0, lastIndex);
}

return currentKcpPath;
}

private getCurrentKcpPath(gatewayUrl: string): string {
const kcpPathRegexp = /\/([^\/]+)\/graphql$/;
const currentKcpPath = gatewayUrl.match(kcpPathRegexp)?.[1];

if (!currentKcpPath) {
this.luigiCoreService.showAlert({
text: 'Could not get current KCP path from gateway URL',
type: 'error',
});

return '';
}

return kcpPath;
return currentKcpPath;
}
}
17 changes: 9 additions & 8 deletions projects/lib/services/resource/resource.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ describe('ResourceService', () => {
});

describe('read', () => {
it('should catch gql parsing error and return null observable', (done) => {
it('should catch gql parsing error and complete the observable', (done) => {
const invalidQuery =
`query { core_k8s_io { TestKind(name: "test-name") {` as unknown as any;

Expand All @@ -80,13 +80,14 @@ describe('ResourceService', () => {
invalidQuery,
namespacedNodeContext,
)
.subscribe((res) => {
expect(res).toBeNull();
expect(mockLuigiCoreService.showAlert).toHaveBeenCalledWith({
text: expect.any(String),
type: 'error',
});
done();
.subscribe({
complete: () => {
expect(mockLuigiCoreService.showAlert).toHaveBeenCalledWith({
text: expect.any(String),
type: 'error',
});
done();
},
});
});

Expand Down
13 changes: 7 additions & 6 deletions projects/lib/services/resource/resource.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
} from '@platform-mesh/portal-ui-lib/utils';
import { gql } from 'apollo-angular';
import * as gqlBuilder from 'gql-query-builder';
import { EMPTY, Observable } from 'rxjs';
import VariableOptions from 'gql-query-builder/build/VariableOptions';
import { Observable, of } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { ApolloFactory } from './apollo-factory';
import { ResourceNodeContext } from './resource-node-context';
Expand Down Expand Up @@ -43,7 +43,7 @@ export class ResourceService {
fieldsOrRawQuery,
kind,
resourceId,
isNamespacedResource ? nodeContext.namespaceId : null,
isNamespacedResource ? nodeContext.namespaceId : undefined,
operation,
);

Expand All @@ -56,7 +56,7 @@ export class ResourceService {
text: `Could not read a resource: ${resourceId}. Wrong read query: <br/><br/> ${query}`,
type: 'error',
});
return of(null);
return EMPTY;
}

return this.apolloFactory
Expand Down Expand Up @@ -84,7 +84,7 @@ export class ResourceService {
fieldsOrRawQuery: any[] | string,
kind: string,
resourceId: string,
namespace: string,
namespace: string | undefined,
operation: string,
) {
if (fieldsOrRawQuery instanceof Array) {
Expand Down Expand Up @@ -144,8 +144,9 @@ export class ResourceService {
variables: query.variables,
})
.pipe(
map((res: any): Resource[] =>
getValueByPath<any, Resource[]>(res.data, operation),
map(
(res: any): Resource[] =>
getValueByPath<any, Resource[]>(res.data, operation) ?? [],
),
catchError((error) => {
this.alertErrors(error);
Expand Down
29 changes: 0 additions & 29 deletions projects/lib/utils/utils/resource-field-by-path.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,6 @@ import { FieldDefinition, Resource } from '@openmfp/portal-ui-lib';
import { getResourceValueByJsonPath } from './resource-field-by-path';

describe('getResourceValueByJsonPath', () => {
it('should return undefined when field is undefined', () => {
const resource = {} as Resource;
const result = getResourceValueByJsonPath(
resource,
undefined as unknown as FieldDefinition,
);
expect(result).toBeUndefined();
});

it('should return undefined when property is not defined', () => {
const resource = {} as Resource;
const field = {} as FieldDefinition;
Expand Down Expand Up @@ -69,26 +60,6 @@ describe('getResourceValueByJsonPath', () => {
expect(result).toBe('item1');
});

it('should handle null resource input', () => {
const field = { property: 'metadata.name' } as FieldDefinition;

const result = getResourceValueByJsonPath(
null as unknown as Resource,
field,
);
expect(result).toBeUndefined();
});

it('should handle undefined resource input', () => {
const field = { property: 'metadata.name' } as FieldDefinition;

const result = getResourceValueByJsonPath(
undefined as unknown as Resource,
field,
);
expect(result).toBeUndefined();
});

it('should handle complex nested paths', () => {
const resource = {
spec: {
Expand Down
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 @@ -5,7 +5,7 @@ export const getResourceValueByJsonPath = (
resource: Resource,
field: { jsonPathExpression?: string; property?: string | string[] },
) => {
const property = field?.jsonPathExpression || field?.property;
const property = field.jsonPathExpression || field.property;
if (!property) {
return undefined;
}
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;
};
Loading