[NIFI-14002] - Quick copy of IDs (#9514)

This commit is contained in:
Rob Fellows 2024-11-13 12:58:13 -05:00 committed by GitHub
parent 6c4ddf8631
commit 919b376c16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 316 additions and 44 deletions

View File

@ -22,7 +22,9 @@
<div class="panel-content flex flex-col h-full w-full gap-y-4">
<div>
<div>Id</div>
<div class="tertiary-color font-medium">{{ actionEntity.sourceId }}</div>
<div [copy]="actionEntity.sourceId" class="tertiary-color font-medium">
{{ actionEntity.sourceId }}
</div>
</div>
@if (actionEntity.action; as action) {

View File

@ -28,13 +28,13 @@ import {
PurgeActionDetails,
RemoteProcessGroupDetails
} from '../../../state/flow-configuration-history-listing';
import { PipesModule, CloseOnEscapeDialog } from '@nifi/shared';
import { PipesModule, CloseOnEscapeDialog, CopyDirective } from '@nifi/shared';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'action-details',
standalone: true,
imports: [CommonModule, MatDialogModule, PipesModule, MatButtonModule],
imports: [CommonModule, MatDialogModule, PipesModule, MatButtonModule, CopyDirective],
templateUrl: './action-details.component.html',
styleUrls: ['./action-details.component.scss']
})

View File

@ -107,7 +107,9 @@
</div>
<div class="flex flex-col mb-5">
<div>Id</div>
<div class="tertiary-color font-medium">{{ dialogRequest.entity.id }}</div>
<div [copy]="dialogRequest.entity.id" class="tertiary-color font-medium">
{{ dialogRequest.entity.id }}
</div>
</div>
<div>
<mat-form-field>

View File

@ -33,7 +33,7 @@ import { MatInputModule } from '@angular/material/input';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nifi-spinner.directive';
import { NifiTooltipDirective, TextTip } from '@nifi/shared';
import { CopyDirective, NifiTooltipDirective, TextTip } from '@nifi/shared';
import { MatTabsModule } from '@angular/material/tabs';
import { ComponentType } from 'libs/shared/src';
import { NiFiState } from '../../../../../../../state';
@ -88,7 +88,8 @@ import { ContextErrorBanner } from '../../../../../../../ui/common/context-error
SourceRemoteProcessGroup,
DestinationRemoteProcessGroup,
ErrorBanner,
ContextErrorBanner
ContextErrorBanner,
CopyDirective
],
templateUrl: './edit-connection.component.html',
styleUrls: ['./edit-connection.component.scss']

View File

@ -41,7 +41,9 @@
</div>
<div class="flex flex-col mb-5">
<div>Id</div>
<div class="tertiary-color font-medium">{{ request.entity.id }}</div>
<div [copy]="request.entity.id" class="tertiary-color font-medium">
{{ request.entity.id }}
</div>
</div>
<div class="flex flex-col mb-5">
<div>Type</div>

View File

@ -45,7 +45,7 @@ import { Client } from '../../../../../../../service/client.service';
import { EditComponentDialogRequest, UpdateProcessorRequest } from '../../../../../state/flow';
import { PropertyTable } from '../../../../../../../ui/common/property-table/property-table.component';
import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nifi-spinner.directive';
import { NifiTooltipDirective, NiFiCommon, TextTip } from '@nifi/shared';
import { NifiTooltipDirective, NiFiCommon, TextTip, CopyDirective } from '@nifi/shared';
import { RunDurationSlider } from './run-duration-slider/run-duration-slider.component';
import {
RelationshipConfiguration,
@ -87,7 +87,8 @@ import { ContextErrorBanner } from '../../../../../../../ui/common/context-error
RelationshipSettings,
ErrorBanner,
PropertyVerification,
ContextErrorBanner
ContextErrorBanner,
CopyDirective
],
styleUrls: ['./edit-processor.component.scss']
})

View File

@ -25,7 +25,9 @@
</div>
<div class="flex flex-col mb-4">
<div>Id</div>
<div class="tertiary-color font-medium">{{ request.entity.component.id }}</div>
<div [copy]="request.entity.component.id" class="tertiary-color font-medium">
{{ request.entity.component.id }}
</div>
</div>
<div class="flex gap-x-4">
<div class="w-full">

View File

@ -30,7 +30,7 @@ import { NifiSpinnerDirective } from '../../../../../../../ui/common/spinner/nif
import { EditComponentDialogRequest } from '../../../../../state/flow';
import { ErrorBanner } from '../../../../../../../ui/common/error-banner/error-banner.component';
import { CanvasUtils } from '../../../../../service/canvas-utils.service';
import { NifiTooltipDirective, TextTip } from '@nifi/shared';
import { CopyDirective, NifiTooltipDirective, TextTip } from '@nifi/shared';
import { CloseOnEscapeDialog } from '@nifi/shared';
import { ErrorContextKey } from '../../../../../../../state/error';
import { ContextErrorBanner } from '../../../../../../../ui/common/context-error-banner/context-error-banner.component';
@ -51,7 +51,8 @@ import { ContextErrorBanner } from '../../../../../../../ui/common/context-error
FormsModule,
ErrorBanner,
NifiTooltipDirective,
ContextErrorBanner
ContextErrorBanner,
CopyDirective
],
styleUrls: ['./edit-remote-process-group.component.scss']
})

View File

@ -90,9 +90,13 @@
@if (!isNew) {
<div class="flex flex-col mb-5">
<div>Id</div>
<div class="tertiary-color font-medium">
{{ request.parameterContext?.id }}
</div>
@if (request.parameterContext) {
<div [copy]="request.parameterContext.id" class="tertiary-color font-medium">
{{ request.parameterContext.id }}
</div>
} @else {
<div class="font-medium unset neutral-color">No value set</div>
}
</div>
}
@if (parameterProvider) {

View File

@ -45,7 +45,7 @@ import { RouterLink } from '@angular/router';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
import { ClusterConnectionService } from '../../../../../service/cluster-connection.service';
import { TabbedDialog } from '../../../../../ui/common/tabbed-dialog/tabbed-dialog.component';
import { NiFiCommon, TextTip, NifiTooltipDirective } from '@nifi/shared';
import { NiFiCommon, TextTip, NifiTooltipDirective, CopyDirective } from '@nifi/shared';
import { ErrorContextKey } from '../../../../../state/error';
import { ContextErrorBanner } from '../../../../../ui/common/context-error-banner/context-error-banner.component';
@ -72,7 +72,8 @@ import { ContextErrorBanner } from '../../../../../ui/common/context-error-banne
RouterLink,
ErrorBanner,
NifiTooltipDirective,
ContextErrorBanner
ContextErrorBanner,
CopyDirective
],
styleUrls: ['./edit-parameter-context.component.scss']
})

View File

@ -30,7 +30,7 @@
<div>UUID</div>
<ng-container
*ngTemplateOutlet="
formatValue;
formatCopyableValue;
context: { $implicit: request.flowfile.uuid }
"></ng-container>
</div>
@ -131,7 +131,7 @@
<div>Identifier</div>
<ng-container
*ngTemplateOutlet="
formatContentValue;
formatCopyableContentValue;
context: { $implicit: request.flowfile.contentClaimIdentifier }
"></ng-container>
</div>
@ -219,6 +219,25 @@
<div class="unset neutral-color">No value set</div>
</ng-template>
</ng-template>
<ng-template #formatCopyableValue let-value let-title="title">
<ng-container *ngIf="value != null; else nullValue">
<ng-container *ngIf="value === ''; else nonEmptyValue">
<div class="unset neutral-color">Empty string set</div>
</ng-container>
<ng-template #nonEmptyValue>
<div [copy]="value" class="tertiary-color font-medium" *ngIf="title == null; else valueWithTitle">
{{ value }}
</div>
<ng-template #valueWithTitle>
<div [copy]="value" class="tertiary-color font-medium" [title]="title">{{ value }}</div>
</ng-template>
</ng-template>
</ng-container>
<ng-template #nullValue>
<div class="unset neutral-color">No value set</div>
</ng-template>
</ng-template>
<ng-template #formatContentValue let-value let-title="title">
<ng-container *ngIf="value != null; else nullValue">
<div class="tertiary-color font-medium" *ngIf="title == null; else valueWithTitle">
@ -232,6 +251,19 @@
<div class="unset neutral-color">No value previously set</div>
</ng-template>
</ng-template>
<ng-template #formatCopyableContentValue let-value let-title="title">
<ng-container *ngIf="value != null; else nullValue">
<div [copy]="value" class="tertiary-color font-medium" *ngIf="title == null; else valueWithTitle">
{{ value }}
</div>
<ng-template #valueWithTitle>
<div [copy]="value" class="tertiary-color font-medium" [title]="title">{{ value }}</div>
</ng-template>
</ng-container>
<ng-template #nullValue>
<div class="unset neutral-color">No value previously set</div>
</ng-template>
</ng-template>
<mat-dialog-actions align="end">
<button mat-flat-button mat-dialog-close>Ok</button>
</mat-dialog-actions>

View File

@ -25,7 +25,7 @@ import { AsyncPipe, KeyValuePipe, NgForOf, NgIf, NgTemplateOutlet } from '@angul
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatTabsModule } from '@angular/material/tabs';
import { FlowFileDialogRequest } from '../../../state/queue-listing';
import { NiFiCommon } from '@nifi/shared';
import { CopyDirective, NiFiCommon } from '@nifi/shared';
import { TabbedDialog } from '../../../../../ui/common/tabbed-dialog/tabbed-dialog.component';
@Component({
@ -46,7 +46,8 @@ import { TabbedDialog } from '../../../../../ui/common/tabbed-dialog/tabbed-dial
MatTabsModule,
NgTemplateOutlet,
FormsModule,
KeyValuePipe
KeyValuePipe,
CopyDirective
]
})
export class FlowFileDialog extends TabbedDialog {

View File

@ -38,7 +38,7 @@
</mat-form-field>
<div class="flex flex-col mb-5">
<div>Id</div>
<div class="tertiary-color font-medium">
<div [copy]="request.flowAnalysisRule.id" class="tertiary-color font-medium">
{{ request.flowAnalysisRule.id }}
</div>
</div>

View File

@ -27,7 +27,7 @@ import { AbstractControl, FormBuilder, FormControl, FormGroup, ReactiveFormsModu
import { Observable, of } from 'rxjs';
import { Client } from '../../../../../service/client.service';
import { InlineServiceCreationRequest, InlineServiceCreationResponse, Property } from '../../../../../state/shared';
import { NiFiCommon, NifiTooltipDirective, TextTip } from '@nifi/shared';
import { CopyDirective, NiFiCommon, NifiTooltipDirective, TextTip } from '@nifi/shared';
import { PropertyTable } from '../../../../../ui/common/property-table/property-table.component';
import { NifiSpinnerDirective } from '../../../../../ui/common/spinner/nifi-spinner.directive';
import {
@ -68,7 +68,8 @@ import { ContextErrorBanner } from '../../../../../ui/common/context-error-banne
FlowAnalysisRuleTable,
ErrorBanner,
PropertyVerification,
ContextErrorBanner
ContextErrorBanner,
CopyDirective
],
styleUrls: ['./edit-flow-analysis-rule.component.scss']
})

View File

@ -38,7 +38,7 @@
</div>
<div class="flex flex-col mb-5">
<div>Id</div>
<div class="tertiary-color font-medium">
<div [copy]="request.parameterProvider.id" class="tertiary-color font-medium">
{{ request.parameterProvider.id }}
</div>
</div>

View File

@ -43,7 +43,7 @@ import { PropertyTable } from '../../../../../ui/common/property-table/property-
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
import { CommonModule } from '@angular/common';
import { ClusterConnectionService } from '../../../../../service/cluster-connection.service';
import { TextTip, NiFiCommon, NifiTooltipDirective } from '@nifi/shared';
import { TextTip, NiFiCommon, NifiTooltipDirective, CopyDirective } from '@nifi/shared';
import {
ConfigVerificationResult,
ModifiedProperties,
@ -72,7 +72,8 @@ import { ContextErrorBanner } from '../../../../../ui/common/context-error-banne
CommonModule,
NifiTooltipDirective,
PropertyVerification,
ContextErrorBanner
ContextErrorBanner,
CopyDirective
],
templateUrl: './edit-parameter-provider.component.html',
styleUrls: ['./edit-parameter-provider.component.scss']

View File

@ -31,7 +31,7 @@
<div class="dialog-tab-content flex flex-col">
<div class="flex flex-col mb-5">
<div>Id</div>
<div class="tertiary-color font-medium">
<div [copy]="request.registryClient.id" class="tertiary-color font-medium">
{{ request.registryClient.id }}
</div>
</div>

View File

@ -33,7 +33,7 @@ import { EditRegistryClientDialogRequest, EditRegistryClientRequest } from '../.
import { NifiSpinnerDirective } from '../../../../../ui/common/spinner/nifi-spinner.directive';
import { Client } from '../../../../../service/client.service';
import { MatSelectModule } from '@angular/material/select';
import { TextTip, NifiTooltipDirective, NiFiCommon } from '@nifi/shared';
import { TextTip, NifiTooltipDirective, NiFiCommon, CopyDirective } from '@nifi/shared';
import { MatTabsModule } from '@angular/material/tabs';
import { PropertyTable } from '../../../../../ui/common/property-table/property-table.component';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
@ -59,7 +59,8 @@ import { ContextErrorBanner } from '../../../../../ui/common/context-error-banne
MatTabsModule,
PropertyTable,
ErrorBanner,
ContextErrorBanner
ContextErrorBanner,
CopyDirective
],
styleUrls: ['./edit-registry-client.component.scss']
})

View File

@ -41,7 +41,7 @@
</div>
<div class="flex flex-col mb-5">
<div>Id</div>
<div class="tertiary-color font-medium">
<div [copy]="request.reportingTask.id" class="tertiary-color font-medium">
{{ request.reportingTask.id }}
</div>
</div>

View File

@ -41,7 +41,7 @@ import {
UpdateReportingTaskRequest
} from '../../../state/reporting-tasks';
import { ControllerServiceApi } from '../../../../../ui/common/controller-service/controller-service-api/controller-service-api.component';
import { NifiTooltipDirective, NiFiCommon, TextTip } from '@nifi/shared';
import { NifiTooltipDirective, NiFiCommon, TextTip, CopyDirective } from '@nifi/shared';
import { ErrorBanner } from '../../../../../ui/common/error-banner/error-banner.component';
import { ClusterConnectionService } from '../../../../../service/cluster-connection.service';
import {
@ -75,7 +75,8 @@ import { ContextErrorBanner } from '../../../../../ui/common/context-error-banne
NifiTooltipDirective,
ErrorBanner,
PropertyVerification,
ContextErrorBanner
ContextErrorBanner,
CopyDirective
],
styleUrls: ['./edit-reporting-task.component.scss']
})

View File

@ -40,7 +40,7 @@
</div>
<div class="flex flex-col mb-5">
<div>Id</div>
<div class="tertiary-color font-medium">
<div [copy]="request.controllerService.id" class="tertiary-color font-medium">
{{ request.controllerService.id }}
</div>
</div>

View File

@ -34,7 +34,7 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatButtonModule } from '@angular/material/button';
import { AsyncPipe } from '@angular/common';
import { MatTabsModule } from '@angular/material/tabs';
import { NiFiCommon, TextTip, NifiTooltipDirective } from '@nifi/shared';
import { NiFiCommon, TextTip, NifiTooltipDirective, CopyDirective } from '@nifi/shared';
import { MatOptionModule } from '@angular/material/core';
import { MatSelectModule } from '@angular/material/select';
import { PropertyTable } from '../../property-table/property-table.component';
@ -76,7 +76,8 @@ import { ContextErrorBanner } from '../../context-error-banner/context-error-ban
ErrorBanner,
NifiTooltipDirective,
PropertyVerification,
ContextErrorBanner
ContextErrorBanner,
CopyDirective
],
styleUrls: ['./edit-controller-service.component.scss']
})

View File

@ -59,7 +59,7 @@
<div>FlowFile UUID</div>
<ng-container
*ngTemplateOutlet="
formatValue;
formatCopyableValue;
context: { $implicit: request.event.flowFileUuid }
"></ng-container>
</div>
@ -78,7 +78,7 @@
<div>Component Id</div>
<ng-container
*ngTemplateOutlet="
formatValue;
formatCopyableValue;
context: { $implicit: request.event.componentId }
"></ng-container>
</div>
@ -114,7 +114,7 @@
<div>Source FlowFile Id</div>
<ng-container
*ngTemplateOutlet="
formatValue;
formatCopyableValue;
context: { $implicit: request.event.sourceSystemFlowFileId }
"></ng-container>
</div>
@ -288,7 +288,7 @@
<div>Identifier</div>
<ng-container
*ngTemplateOutlet="
formatContentValue;
formatCopyableContentValue;
context: { $implicit: request.event.inputContentClaimIdentifier }
"></ng-container>
</div>
@ -350,7 +350,7 @@
<div>Identifier</div>
<ng-container
*ngTemplateOutlet="
formatContentValue;
formatCopyableContentValue;
context: { $implicit: request.event.outputContentClaimIdentifier }
"></ng-container>
</div>
@ -399,7 +399,7 @@
<div>Connection Id</div>
<ng-container
*ngTemplateOutlet="
formatContentValue;
formatCopyableContentValue;
context: { $implicit: request.event.sourceConnectionIdentifier }
"></ng-container>
</div>
@ -443,6 +443,32 @@
<div class="unset neutral-color">No value set</div>
</ng-template>
</ng-template>
<ng-template #formatCopyableValue let-value let-title="title">
<ng-container *ngIf="value != null; else nullValue">
<ng-container *ngIf="value === ''; else nonEmptyValue">
<div class="unset neutral-color">Empty string set</div>
</ng-container>
<ng-template #nonEmptyValue>
<div
[copy]="value"
class="tertiary-color font-medium overflow-ellipsis overflow-hidden whitespace-nowrap"
*ngIf="title == null; else valueWithTitle">
{{ value }}
</div>
<ng-template #valueWithTitle>
<div
[copy]="value"
class="tertiary-color font-medium overflow-ellipsis overflow-hidden whitespace-nowrap"
[title]="title">
{{ value }}
</div>
</ng-template>
</ng-template>
</ng-container>
<ng-template #nullValue>
<div class="unset neutral-color">No value set</div>
</ng-template>
</ng-template>
<ng-template #formatContentValue let-value let-title="title">
<ng-container *ngIf="value != null; else nullValue">
<div
@ -462,6 +488,27 @@
<div class="unset neutral-color">No value previously set</div>
</ng-template>
</ng-template>
<ng-template #formatCopyableContentValue let-value let-title="title">
<ng-container *ngIf="value != null; else nullValue">
<div
[copy]="value"
class="tertiary-color font-medium overflow-ellipsis overflow-hidden whitespace-nowrap"
*ngIf="title == null; else valueWithTitle">
{{ value }}
</div>
<ng-template #valueWithTitle>
<div
[copy]="value"
class="tertiary-color font-medium overflow-ellipsis overflow-hidden whitespace-nowrap"
[title]="title">
{{ value }}
</div>
</ng-template>
</ng-container>
<ng-template #nullValue>
<div class="unset neutral-color">No value previously set</div>
</ng-template>
</ng-template>
<mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Ok</button>

View File

@ -23,7 +23,7 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatButtonModule } from '@angular/material/button';
import { AsyncPipe, NgForOf, NgIf, NgTemplateOutlet } from '@angular/common';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { NiFiCommon } from '@nifi/shared';
import { CopyDirective, NiFiCommon } from '@nifi/shared';
import { MatTabsModule } from '@angular/material/tabs';
import { Attribute, ProvenanceEventDialogRequest } from '../../../state/shared';
import { TabbedDialog } from '../tabbed-dialog/tabbed-dialog.component';
@ -44,7 +44,8 @@ import { TabbedDialog } from '../tabbed-dialog/tabbed-dialog.component';
MatDatepickerModule,
MatTabsModule,
NgTemplateOutlet,
FormsModule
FormsModule,
CopyDirective
]
})
export class ProvenanceEventDialog extends TabbedDialog {

View File

@ -99,6 +99,10 @@
cursor: pointer;
}
.copy-button:hover {
cursor: pointer;
}
.dialog-tab-content,
.dialog-content {
height: 50vh;

View File

@ -0,0 +1,92 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Component } from '@angular/core';
import { By } from '@angular/platform-browser';
import { CopyDirective } from './copy.directive';
import { ComponentFixture, TestBed } from '@angular/core/testing';
@Component({
standalone: true,
template: `<div [copy]="copyText">test</div>`,
imports: [CopyDirective]
})
class TestComponent {
copyText = 'copied value';
}
describe('CopyDirective', () => {
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let directiveDebugEl: any;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [CopyDirective, TestComponent]
});
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
fixture.detectChanges();
directiveDebugEl = fixture.debugElement.query(By.directive(CopyDirective));
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should create a copy button on mouse enter', () => {
directiveDebugEl.triggerEventHandler('mouseenter', null);
fixture.detectChanges();
const copyButton = directiveDebugEl.nativeElement.querySelector('.copy-button');
expect(copyButton).not.toBeNull();
expect(copyButton?.classList).toContain('fa-copy');
});
it('should remove the copy button on mouse leave', () => {
directiveDebugEl.triggerEventHandler('mouseenter', null);
fixture.detectChanges();
directiveDebugEl.triggerEventHandler('mouseleave', null);
fixture.detectChanges();
const copyButton = directiveDebugEl.nativeElement.querySelector('.copy-button');
expect(copyButton).toBeNull();
});
it('should copy text to clipboard and change button appearance on click', async () => {
// Mock the clipboard's writeText method
Object.assign(navigator, {
clipboard: {
writeText: jest.fn().mockResolvedValue(undefined)
}
});
directiveDebugEl.triggerEventHandler('mouseenter', null);
fixture.detectChanges();
const copyButton = directiveDebugEl.nativeElement.querySelector('.copy-button');
copyButton?.dispatchEvent(new MouseEvent('click'));
fixture.detectChanges();
await fixture.whenStable();
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(component.copyText);
expect(copyButton?.classList).toContain('copied');
expect(copyButton?.classList).toContain('fa-check');
expect(copyButton?.classList).toContain('success-color-default');
});
it('should unsubscribe from events on mouse leave', () => {
directiveDebugEl.triggerEventHandler('mouseenter', null);
fixture.detectChanges();
const directiveInstance = directiveDebugEl.injector.get(CopyDirective);
const unsubscribeSpy = jest.spyOn(directiveInstance.subscription!, 'unsubscribe');
directiveDebugEl.triggerEventHandler('mouseleave', null);
fixture.detectChanges();
expect(unsubscribeSpy).toHaveBeenCalled();
unsubscribeSpy.mockRestore();
});
});

View File

@ -0,0 +1,73 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Directive, ElementRef, HostListener, Input, NgZone, Renderer2 } from '@angular/core';
import { fromEvent, Subscription, switchMap, take } from 'rxjs';
@Directive({
selector: '[copy]',
standalone: true
})
export class CopyDirective {
@Input({ required: true }) copy!: string;
private copyButton: HTMLElement | null = null;
private subscription: Subscription | null = null;
constructor(
private elementRef: ElementRef<HTMLElement>,
private renderer: Renderer2,
private zone: NgZone
) {}
@HostListener('mouseenter')
onMouseEnter() {
this.copyButton = this.renderer.createElement('i');
if (this.copyButton) {
const cb: HTMLElement = this.copyButton;
cb.classList.add('copy-button', 'fa', 'fa-copy', 'ml-2', 'primary-color');
// run outside the angular zone to prevent unnecessary change detection cycles
this.subscription = this.zone.runOutsideAngular(() => {
return fromEvent(cb, 'click')
.pipe(
switchMap(() => navigator.clipboard.writeText(this.copy)),
take(1)
)
.subscribe(() => {
cb.classList.remove('copy-button', 'fa-copy');
cb.classList.add('copied', 'fa-check', 'success-color-default');
});
});
this.renderer.appendChild(this.elementRef.nativeElement, this.copyButton);
}
}
@HostListener('mouseleave')
onMouseLeave() {
// if the user leaves the element without clicking, the subscription needs closed
if (this.subscription && !this.subscription.closed) {
this.subscription.unsubscribe();
}
if (this.copyButton) {
this.renderer?.removeChild(this.elementRef.nativeElement, this.copyButton);
this.copyButton = null;
}
}
}

View File

@ -16,3 +16,4 @@
*/
export * from './nifi-tooltip.directive';
export * from './copy/copy.directive';