Skip to content

Commit 20a429b

Browse files
committed
feat: implement secret filed & copy feature
1 parent a9842c1 commit 20a429b

File tree

12 files changed

+386
-62
lines changed

12 files changed

+386
-62
lines changed

projects/lib/models/models/resource.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Condition, ObjectMeta } from 'kubernetes-types/meta/v1';
22

3+
34
export interface LabelDisplay {
45
backgroundColor?: string;
56
color?: string;
@@ -36,6 +37,8 @@ export interface FieldDefinition {
3637
};
3738
labelDisplay?: LabelDisplay | boolean;
3839
displayAsPlainText?: boolean;
40+
withCopyButton?: boolean;
41+
displayAsSecret?: boolean;
3942
dynamicValuesDefinition?: {
4043
operation: string;
4144
gqlQuery: string;
@@ -96,4 +99,4 @@ export interface UIDefinition {
9699
detailView?: UiView;
97100
}
98101

99-
export type KubernetesScope = 'Cluster' | 'Namespaced';
102+
export type KubernetesScope = 'Cluster' | 'Namespaced';

projects/wc/_mocks_/ui5-mock.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Component } from '@angular/core';
22

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

@@ -31,4 +30,8 @@ jest.mock('@ui5/webcomponents-ngx', () => {
3130
BarComponent: MockComponent,
3231
LinkComponent: MockComponent,
3332
};
34-
});
33+
});
34+
35+
jest.mock('@ui5/webcomponents-icons/dist/copy.js', () => ({}), {
36+
virtual: true,
37+
});

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@
4949
<span [class.multiline]="viewField.group.multiline ?? true">
5050
<span>{{ field.label }}: </span>
5151
<value-cell
52-
[labelDisplay]="field.labelDisplay"
53-
[value]="getResourceValueByJsonPath(resource, field)"
54-
[displayAsPlainText]="!!field.displayAsPlainText" />
52+
[fieldDefinition]="field"
53+
[resource]="resource"
54+
[LuigiClient]="LuigiClient()"
55+
/>
5556
</span>
5657
@if (!last && viewField.group.delimiter) {
5758
<span>{{ viewField.group.delimiter }}</span>
@@ -62,9 +63,10 @@
6263
<ui5-label>{{ viewField.label }}</ui5-label>
6364
<p>
6465
<value-cell
65-
[labelDisplay]="viewField.labelDisplay"
66-
[value]="getResourceValueByJsonPath(resource, viewField)"
67-
[displayAsPlainText]="!!viewField.displayAsPlainText" />
66+
[fieldDefinition]="viewField"
67+
[resource]="resource"
68+
[LuigiClient]="LuigiClient()"
69+
/>
6870
</p>
6971
}
7072
</div>

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@
8888
<div>
8989
<span>{{ field.label }}: </span>
9090
<value-cell
91-
[labelDisplay]="field.labelDisplay"
92-
[displayAsPlainText]="!!field.displayAsPlainText"
93-
[value]="getResourceValueByJsonPath(item, field)"
91+
[fieldDefinition]="field"
92+
[resource]="item"
93+
[LuigiClient]="LuigiClient()"
9494
/>
9595
</div>
9696
@if (!last && column.group.delimiter) {
@@ -101,9 +101,9 @@
101101
} @else {
102102
<ui5-table-cell>
103103
<value-cell
104-
[labelDisplay]="column.labelDisplay"
105-
[displayAsPlainText]="!!column.displayAsPlainText"
106-
[value]="getResourceValueByJsonPath(item, column)"
104+
[fieldDefinition]="column"
105+
[resource]="item"
106+
[LuigiClient]="LuigiClient()"
107107
/>
108108
</ui5-table-cell>
109109
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<span class="secret-value">
2+
<span class="masked">{{ maskedValue() }}</span>
3+
<span class="original">{{ value() }}</span>
4+
</span>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.secret-value {
2+
position: relative;
3+
cursor: pointer;
4+
-webkit-tap-highlight-color: transparent;
5+
user-select: none;
6+
display: inline-flex;
7+
8+
.original {
9+
display: none;
10+
}
11+
12+
.masked {
13+
display: inline;
14+
transform: translateY(0.2em);
15+
}
16+
17+
&:hover,
18+
&:active {
19+
.original {
20+
display: inline;
21+
}
22+
23+
.masked {
24+
display: none;
25+
}
26+
}
27+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { SecretValueComponent } from './secret-value.component';
2+
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
3+
import { ComponentFixture, TestBed } from '@angular/core/testing';
4+
5+
describe('SecretValueComponent', () => {
6+
let component: SecretValueComponent;
7+
let fixture: ComponentFixture<SecretValueComponent>;
8+
9+
const makeComponent = (value: string) => {
10+
fixture = TestBed.createComponent(SecretValueComponent);
11+
component = fixture.componentInstance;
12+
13+
fixture.componentRef.setInput('value', value);
14+
15+
fixture.detectChanges();
16+
17+
return { component, fixture };
18+
};
19+
20+
beforeEach(() => {
21+
TestBed.configureTestingModule({
22+
imports: [SecretValueComponent],
23+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
24+
});
25+
});
26+
27+
it('should create', () => {
28+
const { component } = makeComponent('test-secret');
29+
expect(component).toBeTruthy();
30+
});
31+
32+
it('should mask value with asterisks', () => {
33+
const { component, fixture } = makeComponent('my-secret-password');
34+
const compiled = fixture.nativeElement;
35+
36+
expect(component.maskedValue()).toBe(
37+
'*'.repeat('my-secret-password'.length),
38+
);
39+
expect(compiled.querySelector('.masked')?.textContent).toBe(
40+
'*'.repeat('my-secret-password'.length),
41+
);
42+
});
43+
44+
it('should mask empty string with default 8 asterisks', () => {
45+
const { component, fixture } = makeComponent('');
46+
const compiled = fixture.nativeElement;
47+
48+
expect(component.maskedValue()).toBe('*'.repeat(8));
49+
expect(compiled.querySelector('.masked')?.textContent).toBe('*'.repeat(8));
50+
});
51+
52+
it('should mask short value correctly', () => {
53+
const { component, fixture } = makeComponent('abc');
54+
const compiled = fixture.nativeElement;
55+
56+
expect(component.maskedValue()).toBe('***');
57+
expect(compiled.querySelector('.masked')?.textContent).toBe('***');
58+
});
59+
60+
it('should mask long value correctly', () => {
61+
const longValue = 'a'.repeat(100);
62+
const { component, fixture } = makeComponent(longValue);
63+
const compiled = fixture.nativeElement;
64+
65+
expect(component.maskedValue()).toBe('*'.repeat(100));
66+
expect(compiled.querySelector('.masked')?.textContent).toBe(
67+
'*'.repeat(100),
68+
);
69+
});
70+
71+
it('should display original value in hidden span', () => {
72+
const { fixture } = makeComponent('secret-value');
73+
const compiled = fixture.nativeElement;
74+
75+
const originalSpan = compiled.querySelector('.original');
76+
expect(originalSpan).toBeTruthy();
77+
expect(originalSpan?.textContent).toBe('secret-value');
78+
});
79+
80+
it('should update masked value when input changes', () => {
81+
const { component, fixture } = makeComponent('initial');
82+
expect(component.maskedValue()).toBe('*'.repeat('initial'.length));
83+
84+
fixture.componentRef.setInput('value', 'updated-secret');
85+
fixture.detectChanges();
86+
87+
expect(component.maskedValue()).toBe('*'.repeat('updated-secret'.length));
88+
});
89+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Component, computed, input } from '@angular/core';
2+
3+
4+
@Component({
5+
selector: 'wc-secret-value',
6+
imports: [],
7+
templateUrl: './secret-value.component.html',
8+
styleUrl: './secret-value.component.scss',
9+
})
10+
export class SecretValueComponent {
11+
value = input.required<string>();
12+
13+
maskedValue = computed(() => '*'.repeat(this.value().length || 8));
14+
}
Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
@if (displayAsPlainText()) {
22
{{ value() }}
3+
} @else if (displayAsSecret()) {
4+
<wc-secret-value [value]="value()"></wc-secret-value>
35
} @else {
46
@if (isBoolLike()) {
57
<wc-boolean-value [boolValue]="boolValue()!"></wc-boolean-value>
68
} @else if (isUrlValue()) {
79
<wc-link-value [urlValue]="stringValue()!"></wc-link-value>
810
} @else if (isLabelValue()) {
911
<wc-label-value [labelDisplay]="labelDisplayValue()!" [value]="value()"></wc-label-value>
10-
}
11-
@else {
12+
} @else {
1213
{{ value() }}
1314
}
1415
}
16+
17+
@if (withCopyButton()) {
18+
<ui5-icon (click)="copyValue($event)" name="copy">
19+
</ui5-icon>
20+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:host {
2+
display: flex;
3+
align-items: center;
4+
gap: 0.5rem;
5+
}

0 commit comments

Comments
 (0)