Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 3 additions & 1 deletion projects/lib/models/models/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface FieldDefinition {
};
labelDisplay?: LabelDisplay | boolean;
displayAsPlainText?: boolean;
withCopyButton?: boolean;
displayAsSecret?: boolean;
dynamicValuesDefinition?: {
operation: string;
gqlQuery: string;
Expand Down Expand Up @@ -96,4 +98,4 @@ export interface UIDefinition {
detailView?: UiView;
}

export type KubernetesScope = 'Cluster' | 'Namespaced';
export type KubernetesScope = 'Cluster' | 'Namespaced';
7 changes: 5 additions & 2 deletions projects/wc/_mocks_/ui5-mock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Component } from '@angular/core';


@Component({ selector: 'ui5-component', template: '', standalone: true })
export class MockComponent {}

Expand Down Expand Up @@ -31,4 +30,8 @@ jest.mock('@ui5/webcomponents-ngx', () => {
BarComponent: MockComponent,
LinkComponent: MockComponent,
};
});
});

jest.mock('@ui5/webcomponents-icons/dist/copy.js', () => ({}), {
virtual: true,
});
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,10 @@
<span [class.multiline]="viewField.group.multiline ?? true">
<span>{{ field.label }}: </span>
<value-cell
[labelDisplay]="field.labelDisplay"
[value]="getResourceValueByJsonPath(resource, field)"
[displayAsPlainText]="!!field.displayAsPlainText" />
[fieldDefinition]="field"
[resource]="resource"
[LuigiClient]="LuigiClient()"
/>
</span>
@if (!last && viewField.group.delimiter) {
<span>{{ viewField.group.delimiter }}</span>
Expand All @@ -62,9 +63,10 @@
<ui5-label>{{ viewField.label }}</ui5-label>
<p>
<value-cell
[labelDisplay]="viewField.labelDisplay"
[value]="getResourceValueByJsonPath(resource, viewField)"
[displayAsPlainText]="!!viewField.displayAsPlainText" />
[fieldDefinition]="viewField"
[resource]="resource"
[LuigiClient]="LuigiClient()"
/>
</p>
}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,9 @@
<div>
<span>{{ field.label }}: </span>
<value-cell
[labelDisplay]="field.labelDisplay"
[displayAsPlainText]="!!field.displayAsPlainText"
[value]="getResourceValueByJsonPath(item, field)"
[fieldDefinition]="field"
[resource]="item"
[LuigiClient]="LuigiClient()"
/>
</div>
@if (!last && column.group.delimiter) {
Expand All @@ -101,9 +101,9 @@
} @else {
<ui5-table-cell>
<value-cell
[labelDisplay]="column.labelDisplay"
[displayAsPlainText]="!!column.displayAsPlainText"
[value]="getResourceValueByJsonPath(item, column)"
[fieldDefinition]="column"
[resource]="item"
[LuigiClient]="LuigiClient()"
/>
</ui5-table-cell>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<span class="secret-value">
<span class="masked">{{ maskedValue() }}</span>
<span class="original">{{ value() }}</span>
</span>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.secret-value {
position: relative;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
user-select: none;
display: inline-flex;

.original {
display: none;
}

.masked {
display: inline;
transform: translateY(0.2em);
}

&:hover,
&:active {
.original {
display: inline;
}

.masked {
display: none;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { SecretValueComponent } from './secret-value.component';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';

describe('SecretValueComponent', () => {
let component: SecretValueComponent;
let fixture: ComponentFixture<SecretValueComponent>;

const makeComponent = (value: string) => {
fixture = TestBed.createComponent(SecretValueComponent);
component = fixture.componentInstance;

fixture.componentRef.setInput('value', value);

fixture.detectChanges();

return { component, fixture };
};

beforeEach(() => {
TestBed.configureTestingModule({
imports: [SecretValueComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
});
});

it('should create', () => {
const { component } = makeComponent('test-secret');
expect(component).toBeTruthy();
});

it('should mask value with asterisks', () => {
const { component, fixture } = makeComponent('my-secret-password');
const compiled = fixture.nativeElement;

expect(component.maskedValue()).toBe(
'*'.repeat('my-secret-password'.length),
);
expect(compiled.querySelector('.masked')?.textContent).toBe(
'*'.repeat('my-secret-password'.length),
);
});

it('should mask empty string with default 8 asterisks', () => {
const { component, fixture } = makeComponent('');
const compiled = fixture.nativeElement;

expect(component.maskedValue()).toBe('*'.repeat(8));
expect(compiled.querySelector('.masked')?.textContent).toBe('*'.repeat(8));
});

it('should mask short value correctly', () => {
const { component, fixture } = makeComponent('abc');
const compiled = fixture.nativeElement;

expect(component.maskedValue()).toBe('***');
expect(compiled.querySelector('.masked')?.textContent).toBe('***');
});

it('should mask long value correctly', () => {
const longValue = 'a'.repeat(100);
const { component, fixture } = makeComponent(longValue);
const compiled = fixture.nativeElement;

expect(component.maskedValue()).toBe('*'.repeat(100));
expect(compiled.querySelector('.masked')?.textContent).toBe(
'*'.repeat(100),
);
});

it('should display original value in hidden span', () => {
const { fixture } = makeComponent('secret-value');
const compiled = fixture.nativeElement;

const originalSpan = compiled.querySelector('.original');
expect(originalSpan).toBeTruthy();
expect(originalSpan?.textContent).toBe('secret-value');
});

it('should update masked value when input changes', () => {
const { component, fixture } = makeComponent('initial');
expect(component.maskedValue()).toBe('*'.repeat('initial'.length));

fixture.componentRef.setInput('value', 'updated-secret');
fixture.detectChanges();

expect(component.maskedValue()).toBe('*'.repeat('updated-secret'.length));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Component, computed, input } from '@angular/core';


@Component({
selector: 'wc-secret-value',
imports: [],
templateUrl: './secret-value.component.html',
styleUrl: './secret-value.component.scss',
})
export class SecretValueComponent {
value = input.required<string>();
maskedValue = computed(() => '*'.repeat(this.value().length || 8));
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
@if (displayAsPlainText()) {
{{ value() }}
} @else if (displayAsSecret()) {
<wc-secret-value [value]="value()"></wc-secret-value>
} @else {
@if (isBoolLike()) {
<wc-boolean-value [boolValue]="boolValue()!"></wc-boolean-value>
} @else if (isUrlValue()) {
<wc-link-value [urlValue]="stringValue()!"></wc-link-value>
} @else if (isLabelValue()) {
<wc-label-value [labelDisplay]="labelDisplayValue()!" [value]="value()"></wc-label-value>
}
@else {
} @else {
{{ value() }}
}
}

@if (withCopyButton()) {
<ui5-icon (click)="copyValue($event)" name="copy">
</ui5-icon>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:host {
display: flex;
align-items: center;
gap: 0.5rem;
}
Loading