Skip to content

feat: code viewer component #1511

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Mar 30, 2022
Merged
4 changes: 3 additions & 1 deletion projects/common/src/color/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export const enum Color {
Yellow6 = '#facf00',
Yellow7 = '#bd9d00',
Yellow8 = '#6d5b00',
Yellow9 = '#181400'
Yellow9 = '#181400',
Transparent = 'transparent',
OffWhite = '#f6f6f64d'
}

export interface ColorCombination {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Observable } from 'rxjs';

export interface DownloadFileMetadata {
dataSource: Observable<string>; // This should be a stringified data for any file
fileName: string;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@import 'color-palette';

.download-json {
.download-file {
width: 40px;
height: 40px;
display: flex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@ import { fakeAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { createHostFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { MockComponent } from 'ng-mocks';
import { Observable, of } from 'rxjs';
import { of } from 'rxjs';
import { ButtonComponent } from '../button/button.component';
import { IconComponent } from '../icon/icon.component';
import { DownloadJsonComponent } from './download-json.component';
import { DownloadJsonModule } from './download-json.module';
import { DownloadFileMetadata } from './download-file-metadata';
import { DownloadFileComponent } from './download-file.component';
import { DownloadFileModule } from './download-file.module';

describe('Download Json Component', () => {
let spectator: Spectator<DownloadJsonComponent>;
describe('Download File Component', () => {
let spectator: Spectator<DownloadFileComponent>;
const mockElement = document.createElement('a');
const createElementSpy = jest.fn().mockReturnValue(mockElement);

const createHost = createHostFactory({
component: DownloadJsonComponent,
imports: [DownloadJsonModule, RouterTestingModule],
component: DownloadFileComponent,
imports: [DownloadFileModule, RouterTestingModule],
declarations: [MockComponent(ButtonComponent), MockComponent(IconComponent)],
providers: [
mockProvider(Document, {
Expand All @@ -29,32 +30,32 @@ describe('Download Json Component', () => {
shallow: true
});

const dataSource$: Observable<unknown> = of({
spans: []
});
const metadata: DownloadFileMetadata = {
dataSource: of(''),
fileName: 'download.txt'
};

test('should have only download button, when data is not loading', () => {
spectator = createHost(`<ht-download-json [dataSource]="dataSource"></ht-download-json>`, {
spectator = createHost(`<ht-download-file [metadata]="metadata"></ht-download-file>`, {
hostProps: {
dataSource: dataSource$
metadata: metadata
}
});

expect(spectator.query(ButtonComponent)).toExist();
});

test('should download json file', fakeAsync(() => {
spectator = createHost(`<ht-download-json [dataSource]="dataSource"></ht-download-json>`, {
test('should download file', fakeAsync(() => {
spectator = createHost(`<ht-download-file [metadata]="metadata"></ht-download-file>`, {
hostProps: {
dataSource: dataSource$
metadata: metadata
}
});

spyOn(spectator.component, 'triggerDownload');

expect(spectator.component.dataLoading).toBe(false);
expect(spectator.component.fileName).toBe('download.json');
const element = spectator.query('.download-json');
const element = spectator.query('.download-file');
expect(element).toExist();

spectator.click(element!);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { DOCUMENT } from '@angular/common';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, Renderer2 } from '@angular/core';
import { IconType } from '@hypertrace/assets-library';
import { IconSize } from '@hypertrace/components';
import { Observable } from 'rxjs';
import { catchError, finalize, take } from 'rxjs/operators';
import { ButtonSize, ButtonStyle } from '../button/button';
import { IconSize } from '../icon/icon-size';
import { NotificationService } from '../notification/notification.service';
import { DownloadFileMetadata } from './download-file-metadata';

@Component({
selector: 'ht-download-json',
selector: 'ht-download-file',
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./download-json.component.scss'],
styleUrls: ['./download-file.component.scss'],
template: `
<div class="download-json" (click)="this.triggerDownload()">
<div *ngIf="this.metadata" class="download-file" (click)="this.triggerDownload()">
<ht-button
*ngIf="!this.dataLoading"
class="download-button"
Expand All @@ -24,12 +24,9 @@ import { NotificationService } from '../notification/notification.service';
</div>
`
})
export class DownloadJsonComponent {
export class DownloadFileComponent {
@Input()
public dataSource!: Observable<unknown>;

@Input()
public fileName: string = 'download.json';
public metadata?: DownloadFileMetadata;

public dataLoading: boolean = false;
private readonly dlJsonAnchorElement: HTMLAnchorElement;
Expand All @@ -45,31 +42,25 @@ export class DownloadJsonComponent {

public triggerDownload(): void {
this.dataLoading = true;
this.dataSource
.pipe(
take(1),
catchError(() => this.notificationService.createFailureToast('Download failed')),
finalize(() => {
this.dataLoading = false;
this.changeDetector.detectChanges();
})
)
.subscribe((data: unknown) => {
if (typeof data === 'string') {
this.downloadData(data);
} else {
this.downloadData(JSON.stringify(data));
}
});
this.metadata!.dataSource.pipe(
take(1),
catchError(() => this.notificationService.createFailureToast('Download failed')),
finalize(() => {
this.dataLoading = false;
this.changeDetector.detectChanges();
})
).subscribe((data: string) => {
this.downloadData(data);
});
}

private downloadData(data: string): void {
this.renderer.setAttribute(
this.dlJsonAnchorElement,
'href',
`data:text/json;charset=utf-8,${encodeURIComponent(data)}`
`data:text/plain;charset=utf-8,${encodeURIComponent(data)}`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this change? How would this change the original json download?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have yaml as well to download, I moved to plain text, I does not affect original download

);
this.renderer.setAttribute(this.dlJsonAnchorElement, 'download', this.fileName);
this.renderer.setAttribute(this.dlJsonAnchorElement, 'download', this.metadata!.fileName);
this.renderer.setAttribute(this.dlJsonAnchorElement, 'display', 'none');
this.dlJsonAnchorElement.click();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { NgModule } from '@angular/core';
import { ButtonModule } from '../button/button.module';
import { IconModule } from '../icon/icon.module';
import { NotificationModule } from '../notification/notification.module';
import { DownloadJsonComponent } from './download-json.component';
import { DownloadFileComponent } from './download-file.component';

@NgModule({
declarations: [DownloadJsonComponent],
declarations: [DownloadFileComponent],
imports: [CommonModule, ButtonModule, NotificationModule, IconModule],
exports: [DownloadJsonComponent]
exports: [DownloadFileComponent]
})
export class DownloadJsonModule {}
export class DownloadFileModule {}
15 changes: 8 additions & 7 deletions projects/components/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export * from './checkbox/checkbox.module';
export * from './collapsible-sidebar/collapsible-sidebar.component';
export * from './collapsible-sidebar/collapsible-sidebar.module';

// Collapsible sidebar
export * from './viewer/code-viewer/code-viewer.component';
export * from './viewer/code-viewer/code-viewer.module';

// Combo Box
export * from './combo-box/combo-box.module';
export * from './combo-box/combo-box.component';
Expand Down Expand Up @@ -75,9 +79,10 @@ export { MenuDropdownComponent } from './menu-dropdown/menu-dropdown.component';
export { MenuItemComponent } from './menu-dropdown/menu-item/menu-item.component';
export { MenuDropdownModule } from './menu-dropdown/menu-dropdown.module';

// Download JSON
export * from './download-json/download-json.component';
export * from './download-json/download-json.module';
// Download File
export * from './download-file/download-file.component';
export * from './download-file/download-file-metadata';
export * from './download-file/download-file.module';

// Dynamic label
export * from './highlighted-label/highlighted-label.component';
Expand Down Expand Up @@ -272,10 +277,6 @@ export * from './overlay/overlay';
export * from './overlay/overlay.module';
export * from './overlay/sheet/sheet';

// Snippet
export { SnippetViewerComponent } from './viewer/snippet-viewer/snippet-viewer.component';
export { SnippetViewerModule } from './viewer/snippet-viewer/snippet-viewer.module';

// Spinner
export * from './spinner/spinner.component';
export * from './spinner/spinner.module';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
@import 'mixins';

@mixin line-base {
display: flex;
align-items: center;
width: 100%;
height: 20px;

&.line-highlight {
background-color: $blue-2;
}
}

.code-viewer {
@include fill-container;
display: grid;
grid-template-rows: 54px auto;

.header {
height: 100%;
display: flex;
align-items: center;
padding: 0 12px;
border-bottom: 1px solid $gray-2;

.title {
@include overline;
}

.header-content {
min-width: 0;
flex: 1 1 auto;
display: flex;
justify-content: flex-end;

.search-box {
width: 140px;
background-color: white;
}
}
}

.content {
@include code;
height: 100%;
overflow-y: auto;
display: grid;
grid-template-columns: 40px auto;
position: relative;

.line-numbers {
width: 100%;
display: flex;
flex-direction: column;

.line-number {
@include line-base;
padding-left: 8px;
}
}

.code-lines {
width: 100%;
display: flex;
flex-direction: column;
overflow-x: auto;

.code-line {
@include line-base;
white-space: break-spaces;

::ng-deep {
mark {
background-color: $yellow-4;
}
}
}
}

.copy-to-clipboard {
position: absolute;
color: $gray-9;
right: 0;
top: 0;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { FormattingModule } from '@hypertrace/common';
import { createComponentFactory } from '@ngneat/spectator/jest';
import { MockComponent } from 'ng-mocks';
import { CopyToClipboardComponent } from '../../copy-to-clipboard/copy-to-clipboard.component';
import { DownloadFileComponent } from '../../download-file/download-file.component';
import { SearchBoxComponent } from '../../search-box/search-box.component';
import { CodeViewerComponent } from './code-viewer.component';

describe('Code Viewer Component', () => {
const createComponent = createComponentFactory({
component: CodeViewerComponent,
declarations: [
MockComponent(SearchBoxComponent),
MockComponent(DownloadFileComponent),
MockComponent(CopyToClipboardComponent)
],
imports: [FormattingModule],
shallow: true
});
const code = `{\n "key": "value" \n }`;
const downloadFileName = 'code.json';

test('should render everything correctly', () => {
const spectator = createComponent({
props: {
code: ''
}
});
expect(spectator.query('.code-viewer')).not.toExist();

// Set code
spectator.setInput({
code: code
});

expect(spectator.query('.code-viewer')).toExist();
expect(spectator.query(SearchBoxComponent)).toExist();
expect(spectator.query(DownloadFileComponent)).not.toExist();
expect(spectator.query(CopyToClipboardComponent)).not.toExist();
expect(spectator.query('.title')).toHaveText('Code Viewer');
expect(spectator.queryAll('.line-number').length).toBe(3);
expect(spectator.queryAll('.code-line').length).toBe(3);
expect(spectator.queryAll('.line-number.line-highlight').length).toBe(0);
expect(spectator.queryAll('.code-line.line-highlight').length).toBe(0);

// Highlight text
spectator.setInput({
highlightText: 'key'
});

expect(spectator.queryAll('.code-line.line-highlight').length).toBe(1);
expect(spectator.queryAll('.code-line.line-highlight').length).toBe(1);

// Download
spectator.setInput({
downloadFileName: downloadFileName
});

expect(spectator.query(DownloadFileComponent)).toExist();
expect(spectator.query(DownloadFileComponent)?.metadata?.fileName).toBe(downloadFileName);

// Copy to clipboard
spectator.setInput({
enableCopy: true
});

expect(spectator.query(CopyToClipboardComponent)).toExist();

// Search
spectator.triggerEventHandler(SearchBoxComponent, 'valueChange', 'e');
expect(spectator.queryAll('mark').length).toBe(2);
});
});
Loading