Skip to content

Commit c37eb6b

Browse files
authored
Merge pull request #82 from platform-mesh/generic-list-ready-status
Generic list ready status
2 parents b327c80 + 7b74398 commit c37eb6b

File tree

6 files changed

+311
-6
lines changed

6 files changed

+311
-6
lines changed

projects/lib/models/models/resource.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export interface Resource extends Record<string, any> {
5454
spec?: ResourceSpec;
5555
status?: ResourceStatus;
5656
__typename?: string;
57+
ready?: boolean;
5758
}
5859

5960
export interface ResourceDefinition {

projects/wc/src/app/components/generic-ui/list-view/list-view.component.html

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
<ui5-table>
3737
<ui5-table-header-row slot="headerRow">
38+
<ui5-table-header-cell width="32px" class="not-ready-indicator"></ui5-table-header-cell>
3839
@for (column of viewColomns(); track column.property) {
3940
@if (column.group) {
4041
<ui5-table-header-cell>{{ column.group.label ?? column.group.name }}</ui5-table-header-cell>
@@ -54,8 +55,13 @@
5455
</ui5-illustrated-message>
5556

5657
@for (item of resources(); track item.metadata.name) {
57-
<ui5-table-row interactive (click)="navigateToResource(item)">
58-
@for (column of viewColomns(); track column.label) {
58+
<ui5-table-row [interactive]="item.ready" [class.disabled]="!item.ready" (click)="navigateToResource(item)">
59+
<ui5-table-cell>
60+
@if (!item.ready) {
61+
<ui5-icon class="not-ready-indicator" (click)="$event.stopPropagation()" [showTooltip]="true" accessibleName="Resource is not ready" name="alert" design="Critical"></ui5-icon>
62+
}
63+
</ui5-table-cell>
64+
@for (column of viewColomns(); let first = $first; track column.label) {
5965
@if (column.group) {
6066
<ui5-table-cell [class.multiline]="column.group.multiline ?? true">
6167
@for (field of column.group.fields; let last = $last; track field.label) {

projects/wc/src/app/components/generic-ui/list-view/list-view.component.scss

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
.actions-column ui5-icon {
3434
min-width: 1rem;
35+
cursor: pointer;
3536
}
3637

3738
.edit-item {
@@ -41,3 +42,20 @@
4142
.delete-item {
4243
padding: 0 0.5rem;
4344
}
45+
46+
.disabled {
47+
filter: brightness(0.92);
48+
pointer-events: none;
49+
50+
ui5-table-cell:not(:first-child) {
51+
color: var(--sapContent_DisabledTextColor, #6a6d70);
52+
53+
ui5-icon {
54+
color: var(--sapContent_DisabledTextColor, #6a6d70);
55+
}
56+
}
57+
}
58+
59+
.not-ready-indicator {
60+
pointer-events: auto;
61+
}

projects/wc/src/app/components/generic-ui/list-view/list-view.component.spec.ts

Lines changed: 261 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ListViewComponent } from './list-view.component';
2+
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
23
import { ComponentFixture, TestBed } from '@angular/core/testing';
34
import { LuigiCoreService } from '@openmfp/portal-ui-lib';
45
import { ResourceService } from '@platform-mesh/portal-ui-lib/services';
@@ -12,7 +13,16 @@ describe('ListViewComponent', () => {
1213

1314
beforeEach(() => {
1415
mockResourceService = {
15-
list: jest.fn().mockReturnValue(of([{ metadata: { name: 'test' } }])),
16+
list: jest.fn().mockReturnValue(
17+
of([
18+
{
19+
metadata: { name: 'test' },
20+
status: {
21+
conditions: [{ type: 'Ready', status: 'True' }],
22+
},
23+
},
24+
]),
25+
),
1626
delete: jest.fn().mockReturnValue(of({})),
1727
create: jest.fn().mockReturnValue(of({ data: { name: 'test' } })),
1828
update: jest.fn().mockReturnValue(of({ data: { name: 'test' } })),
@@ -28,6 +38,7 @@ describe('ListViewComponent', () => {
2838
{ provide: ResourceService, useValue: mockResourceService },
2939
{ provide: LuigiCoreService, useValue: mockLuigiCoreService },
3040
],
41+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
3142
}).overrideComponent(ListViewComponent, {
3243
set: { template: '' },
3344
});
@@ -289,4 +300,253 @@ describe('ListViewComponent', () => {
289300
// Component should still be created even if list fails
290301
expect(newComponent).toBeTruthy();
291302
});
303+
304+
describe('Ready Status Functionality', () => {
305+
it('should mark resource as ready when Ready condition status is True', () => {
306+
const readyResource = {
307+
metadata: { name: 'ready-resource' },
308+
status: {
309+
conditions: [{ type: 'Ready', status: 'True' }],
310+
},
311+
};
312+
313+
mockResourceService.list.mockReturnValueOnce(of([readyResource]));
314+
315+
const newFixture = TestBed.createComponent(ListViewComponent);
316+
const newComponent = newFixture.componentInstance;
317+
318+
newComponent.context = (() => ({
319+
resourceDefinition: {
320+
plural: 'clusters',
321+
kind: 'Cluster',
322+
group: 'core.k8s.io',
323+
ui: {
324+
listView: {
325+
fields: [],
326+
},
327+
},
328+
},
329+
})) as any;
330+
331+
newComponent.LuigiClient = (() => ({
332+
linkManager: () => ({
333+
fromContext: jest.fn().mockReturnThis(),
334+
navigate: jest.fn(),
335+
withParams: jest.fn().mockReturnThis(),
336+
}),
337+
getNodeParams: jest.fn(),
338+
})) as any;
339+
340+
newFixture.detectChanges();
341+
342+
const resources = newComponent.resources();
343+
expect(resources).toHaveLength(1);
344+
expect(resources[0].ready).toBe(true);
345+
expect(resources[0].metadata.name).toBe('ready-resource');
346+
});
347+
348+
it('should mark resource as not ready when Ready condition status is False', () => {
349+
const notReadyResource = {
350+
metadata: { name: 'not-ready-resource' },
351+
status: {
352+
conditions: [{ type: 'Ready', status: 'False' }],
353+
},
354+
};
355+
356+
mockResourceService.list.mockReturnValueOnce(of([notReadyResource]));
357+
358+
const newFixture = TestBed.createComponent(ListViewComponent);
359+
const newComponent = newFixture.componentInstance;
360+
361+
newComponent.context = (() => ({
362+
resourceDefinition: {
363+
plural: 'clusters',
364+
kind: 'Cluster',
365+
group: 'core.k8s.io',
366+
ui: {
367+
listView: {
368+
fields: [],
369+
},
370+
},
371+
},
372+
})) as any;
373+
374+
newComponent.LuigiClient = (() => ({
375+
linkManager: () => ({
376+
fromContext: jest.fn().mockReturnThis(),
377+
navigate: jest.fn(),
378+
withParams: jest.fn().mockReturnThis(),
379+
}),
380+
getNodeParams: jest.fn(),
381+
})) as any;
382+
383+
newFixture.detectChanges();
384+
385+
const resources = newComponent.resources();
386+
expect(resources).toHaveLength(1);
387+
expect(resources[0].ready).toBe(false);
388+
expect(resources[0].metadata.name).toBe('not-ready-resource');
389+
});
390+
391+
it('should mark resource as not ready when Ready condition is missing', () => {
392+
const resourceWithoutReadyCondition = {
393+
metadata: { name: 'no-ready-condition' },
394+
status: {
395+
conditions: [{ type: 'Other', status: 'True' }],
396+
},
397+
};
398+
399+
mockResourceService.list.mockReturnValueOnce(
400+
of([resourceWithoutReadyCondition]),
401+
);
402+
403+
const newFixture = TestBed.createComponent(ListViewComponent);
404+
const newComponent = newFixture.componentInstance;
405+
406+
newComponent.context = (() => ({
407+
resourceDefinition: {
408+
plural: 'clusters',
409+
kind: 'Cluster',
410+
group: 'core.k8s.io',
411+
ui: {
412+
listView: {
413+
fields: [],
414+
},
415+
},
416+
},
417+
})) as any;
418+
419+
newComponent.LuigiClient = (() => ({
420+
linkManager: () => ({
421+
fromContext: jest.fn().mockReturnThis(),
422+
navigate: jest.fn(),
423+
withParams: jest.fn().mockReturnThis(),
424+
}),
425+
getNodeParams: jest.fn(),
426+
})) as any;
427+
428+
newFixture.detectChanges();
429+
430+
const resources = newComponent.resources();
431+
expect(resources).toHaveLength(1);
432+
expect(resources[0].ready).toBe(false);
433+
expect(resources[0].metadata.name).toBe('no-ready-condition');
434+
});
435+
436+
it('should mark resource as not ready when status.conditions is missing', async () => {
437+
const resourceWithoutConditions = {
438+
metadata: { name: 'no-conditions' },
439+
status: {
440+
conditions: [],
441+
},
442+
};
443+
444+
mockResourceService.list.mockReturnValueOnce(
445+
of([resourceWithoutConditions]),
446+
);
447+
448+
const newFixture = TestBed.createComponent(ListViewComponent);
449+
const newComponent = newFixture.componentInstance;
450+
451+
newComponent.context = (() => ({
452+
resourceDefinition: {
453+
plural: 'clusters',
454+
kind: 'Cluster',
455+
group: 'core.k8s.io',
456+
ui: {
457+
listView: {
458+
fields: [],
459+
},
460+
},
461+
},
462+
})) as any;
463+
464+
newComponent.LuigiClient = (() => ({
465+
linkManager: () => ({
466+
fromContext: jest.fn().mockReturnThis(),
467+
navigate: jest.fn(),
468+
withParams: jest.fn().mockReturnThis(),
469+
}),
470+
getNodeParams: jest.fn(),
471+
})) as any;
472+
473+
newFixture.detectChanges();
474+
await newFixture.whenStable();
475+
476+
const resources = newComponent.resources();
477+
expect(resources).toHaveLength(1);
478+
expect(resources[0].ready).toBe(false);
479+
expect(resources[0].metadata.name).toBe('no-conditions');
480+
});
481+
482+
it('should handle mixed ready statuses in resource list', () => {
483+
const mixedResources = [
484+
{
485+
metadata: { name: 'ready-1' },
486+
status: {
487+
conditions: [{ type: 'Ready', status: 'True' }],
488+
},
489+
},
490+
{
491+
metadata: { name: 'not-ready-1' },
492+
status: {
493+
conditions: [{ type: 'Ready', status: 'False' }],
494+
},
495+
},
496+
{
497+
metadata: { name: 'ready-2' },
498+
status: {
499+
conditions: [{ type: 'Ready', status: 'True' }],
500+
},
501+
},
502+
];
503+
504+
mockResourceService.list.mockReturnValueOnce(of(mixedResources));
505+
506+
const newFixture = TestBed.createComponent(ListViewComponent);
507+
const newComponent = newFixture.componentInstance;
508+
509+
newComponent.context = (() => ({
510+
resourceDefinition: {
511+
plural: 'clusters',
512+
kind: 'Cluster',
513+
group: 'core.k8s.io',
514+
ui: {
515+
listView: {
516+
fields: [],
517+
},
518+
},
519+
},
520+
})) as any;
521+
522+
newComponent.LuigiClient = (() => ({
523+
linkManager: () => ({
524+
fromContext: jest.fn().mockReturnThis(),
525+
navigate: jest.fn(),
526+
withParams: jest.fn().mockReturnThis(),
527+
}),
528+
getNodeParams: jest.fn(),
529+
})) as any;
530+
531+
newFixture.detectChanges();
532+
533+
const resources = newComponent.resources();
534+
expect(resources).toHaveLength(3);
535+
expect(resources[0].ready).toBe(true);
536+
expect(resources[1].ready).toBe(false);
537+
expect(resources[2].ready).toBe(true);
538+
});
539+
540+
it('should call generateGqlFieldsWithStatusProperties when listing resources', () => {
541+
const generateGqlFieldsSpy = jest.spyOn(
542+
component as any,
543+
'generateGqlFieldsWithStatusProperties',
544+
);
545+
546+
component.list();
547+
548+
expect(generateGqlFieldsSpy).toHaveBeenCalled();
549+
generateGqlFieldsSpy.mockRestore();
550+
});
551+
});
292552
});

projects/wc/src/app/components/generic-ui/list-view/list-view.component.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,15 +116,25 @@ export class ListViewComponent implements OnInit {
116116
ngOnInit(): void {}
117117

118118
list() {
119-
const fields = generateGraphQLFields(this.columns());
119+
const fields = this.generateGqlFieldsWithStatusProperties();
120120
const queryOperation = `${replaceDotsAndHyphensWithUnderscores(this.resourceDefinition().group)}_${this.resourceDefinition().plural}`;
121121

122122
this.resourceService
123123
.list(queryOperation, fields, this.context())
124124
.pipe(takeUntilDestroyed(this.destroyRef))
125125
.subscribe({
126-
next: (result) => {
127-
this.resources.set(result);
126+
next: (result: any[]) => {
127+
this.resources.set(
128+
result.map((resource) => {
129+
return {
130+
...resource,
131+
ready:
132+
resource.status?.conditions?.find(
133+
(condition: any) => condition.type === 'Ready',
134+
)?.status === 'True',
135+
};
136+
}),
137+
);
128138
},
129139
});
130140
}
@@ -202,4 +212,12 @@ export class ListViewComponent implements OnInit {
202212
event.stopPropagation?.();
203213
this.deleteModal()?.open(resource);
204214
}
215+
216+
private generateGqlFieldsWithStatusProperties() {
217+
return generateGraphQLFields(
218+
this.columns().concat({
219+
property: ['status.conditions.status', 'status.conditions.type'],
220+
}),
221+
);
222+
}
205223
}

0 commit comments

Comments
 (0)