Skip to content

Commit bc40c50

Browse files
dmitriy-borzenkoDmitriy Borzenko
and
Dmitriy Borzenko
authored
Kamu UI 455 improved sql support in readme markdown (#472)
* Added 'Run' button for markdown component * Fixed unit tests * changed CHANGELOG.md * modified script * Added unit tests --------- Co-authored-by: Dmitriy Borzenko <[email protected]>
1 parent 47dc05c commit bc40c50

12 files changed

+140
-28
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## Unreleased
99
### Added
1010
- Added a new page `Query` that allows you to make sql queries without being tied to a dataset
11+
- Impovements for readme section
12+
- Add syntax highlight support for SQL in markdown
13+
- Add copy to clipboard button for SQL blocks
14+
- Add run button that opens "Data" tab in new window and executes the selected query
1115

1216

1317
## [0.29.0] - 2024-11-12

angular.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,14 @@
6060
],
6161
"scripts": [
6262
"node_modules/prismjs/prism.js",
63-
"node_modules/prismjs/components/prism-csharp.min.js",
63+
"node_modules/prismjs/components/prism-sql.min.js",
6464
"node_modules/prismjs/components/prism-css.min.js",
6565
"node_modules/prismjs/plugins/line-numbers/prism-line-numbers.js",
6666
"node_modules/prismjs/plugins/line-highlight/prism-line-highlight.js",
6767
"node_modules/prismjs/plugins/command-line/prism-command-line.js",
6868
"node_modules/emoji-toolkit/lib/js/joypixels.min.js",
69-
"node_modules/katex/dist/katex.min.js"
69+
"node_modules/katex/dist/katex.min.js",
70+
"node_modules/clipboard/dist/clipboard.min.js"
7071
],
7172
"vendorChunk": true,
7273
"extractLicenses": false,
@@ -143,13 +144,13 @@
143144
],
144145
"scripts": [
145146
"node_modules/prismjs/prism.js",
146-
"node_modules/prismjs/components/prism-csharp.min.js",
147147
"node_modules/prismjs/components/prism-css.min.js",
148148
"node_modules/prismjs/plugins/line-numbers/prism-line-numbers.js",
149149
"node_modules/prismjs/plugins/line-highlight/prism-line-highlight.js",
150150
"node_modules/prismjs/plugins/command-line/prism-command-line.js",
151151
"node_modules/emoji-toolkit/lib/js/joypixels.min.js",
152-
"node_modules/katex/dist/katex.min.js"
152+
"node_modules/katex/dist/katex.min.js",
153+
"node_modules/clipboard/dist/clipboard.min.js"
153154
]
154155
}
155156
}

package-lock.json

Lines changed: 7 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"autoprefixer": "^10.4.5",
5353
"bootstrap": "^5.3.3",
5454
"bootstrap-icons": "^1.11.3",
55+
"clipboard": "^2.0.11",
5556
"cron-parser": "^4.9.0",
5657
"d3-scale": "^4.0.2",
5758
"graphql": "^16.8.1",

src/app/dataset-view/additional-components/overview-component/components/readme-section/readme-section.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ <h2 class="box-title align-items-center m-0">
5656
<textarea class="variable-textarea" [(ngModel)]="readmeState"></textarea>
5757
</ng-template>
5858
<ng-template [ngIf]="!isEditView || !editingInProgress">
59-
<markdown class="variable-binding" [data]="readmeState" />
59+
<markdown clipboard class="variable-binding" [data]="readmeState" />
6060
</ng-template>
6161
</div>
6262
</div>

src/app/dataset-view/additional-components/overview-component/components/readme-section/readme-section.component.spec.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { NavigationService } from "src/app/services/navigation.service";
12
import { ComponentFixture, TestBed, fakeAsync, flush, tick } from "@angular/core/testing";
23
import { ReadmeSectionComponent } from "./readme-section.component";
34
import { mockDatasetBasicsDerivedFragment } from "src/app/search/mock.data";
@@ -9,19 +10,21 @@ import { SecurityContext, SimpleChanges } from "@angular/core";
910
import { HttpClient } from "@angular/common/http";
1011
import { HttpClientTestingModule } from "@angular/common/http/testing";
1112
import { FormsModule } from "@angular/forms";
12-
import { AngularSvgIconModule } from "angular-svg-icon";
13+
import { AngularSvgIconModule, SvgIconRegistryService } from "angular-svg-icon";
1314
import { MarkdownModule } from "ngx-markdown";
14-
import { emitClickOnElementByDataTestId } from "src/app/common/base-test.helpers.spec";
15+
import { emitClickOnElementByDataTestId, findNativeElement } from "src/app/common/base-test.helpers.spec";
1516
import { EditMode } from "./readme-section.types";
1617
import { of } from "rxjs";
1718
import { LoggedUserService } from "src/app/auth/logged-user.service";
1819
import { mockAccountDetails } from "src/app/api/mock/auth.mock";
20+
import { DatasetViewTypeEnum } from "src/app/dataset-view/dataset-view.interface";
1921

2022
describe("ReadmeSectionComponent", () => {
2123
let component: ReadmeSectionComponent;
2224
let fixture: ComponentFixture<ReadmeSectionComponent>;
2325
let datasetCommitService: DatasetCommitService;
2426
let loggedUserService: LoggedUserService;
27+
let navigationService: NavigationService;
2528

2629
const mockReadmeContent = "Mock README.md content";
2730

@@ -43,29 +46,34 @@ describe("ReadmeSectionComponent", () => {
4346
],
4447
}).compileComponents();
4548

49+
const iconRegistryService: SvgIconRegistryService = TestBed.inject(SvgIconRegistryService);
50+
iconRegistryService.addSvg("code-square", "");
51+
iconRegistryService.addSvg("pencil", "");
52+
4653
fixture = TestBed.createComponent(ReadmeSectionComponent);
4754
component = fixture.componentInstance;
4855
datasetCommitService = TestBed.inject(DatasetCommitService);
4956
loggedUserService = TestBed.inject(LoggedUserService);
57+
navigationService = TestBed.inject(NavigationService);
5058
component.datasetBasics = mockDatasetBasicsDerivedFragment;
5159
component.currentReadme = mockReadmeContent;
5260
spyOnProperty(loggedUserService, "currentlyLoggedInUser", "get").and.returnValue(mockAccountDetails);
53-
54-
fixture.detectChanges();
5561
});
5662

5763
it("should create", () => {
5864
expect(component).toBeTruthy();
5965
});
6066

6167
it("should check show select tab", () => {
68+
fixture.detectChanges();
6269
expect(component.editingInProgress).toEqual(false);
6370
emitClickOnElementByDataTestId(fixture, "show-edit-tabs");
6471
fixture.detectChanges();
6572
expect(component.editingInProgress).toEqual(true);
6673
});
6774

6875
it("should check switch edit/preview mode", () => {
76+
fixture.detectChanges();
6977
emitClickOnElementByDataTestId(fixture, "show-edit-tabs");
7078
fixture.detectChanges();
7179
expect(component.viewMode).toEqual(EditMode.Edit);
@@ -76,13 +84,15 @@ describe("ReadmeSectionComponent", () => {
7684
});
7785

7886
it("should check push button 'cancel changes' when currentReadme exist", () => {
87+
fixture.detectChanges();
7988
emitClickOnElementByDataTestId(fixture, "show-edit-tabs");
8089
fixture.detectChanges();
8190
emitClickOnElementByDataTestId(fixture, "cancel-changes");
8291
expect(component.readmeState).toEqual(mockReadmeContent);
8392
});
8493

8594
it("should check push button 'cancel changes' when currentReadme is not exist", () => {
95+
fixture.detectChanges();
8696
component.currentReadme = null;
8797
emitClickOnElementByDataTestId(fixture, "show-edit-tabs");
8898
fixture.detectChanges();
@@ -91,6 +101,7 @@ describe("ReadmeSectionComponent", () => {
91101
});
92102

93103
it("should check save changes", fakeAsync(() => {
104+
fixture.detectChanges();
94105
component.readmeState = mockReadmeContent + "modified";
95106
const updateReadmeSpy = spyOn(datasetCommitService, "updateReadme").and.returnValue(of());
96107
emitClickOnElementByDataTestId(fixture, "show-edit-tabs");
@@ -114,4 +125,30 @@ describe("ReadmeSectionComponent", () => {
114125
component.ngOnChanges(readmeSimpleChanges);
115126
expect(component.readmeState).toEqual(modifiedReadmeContent);
116127
});
128+
129+
it("should check Run and Copy buttons exist", () => {
130+
component.readmeState = "```sql" + "\nselect * from 'account.tokens.portfolio.market-value'" + "\n```";
131+
component.viewMode = EditMode.Preview;
132+
fixture.detectChanges();
133+
const copyButtonElement = findNativeElement(fixture, `.markdown-clipboard-button`);
134+
expect(copyButtonElement).toBeDefined();
135+
136+
const runButtonElement = findNativeElement(fixture, `.markdown-run-button`);
137+
expect(runButtonElement).toBeDefined();
138+
});
139+
140+
it("should check Run button navigate to Data tab", () => {
141+
component.readmeState = "```sql" + "\nselect * from 'account.tokens.portfolio.market-value'" + "\n```";
142+
component.viewMode = EditMode.Preview;
143+
144+
const navigateToDatasetViewSpy = spyOn(navigationService, "navigateToDatasetView");
145+
fixture.detectChanges();
146+
147+
const runButtonElement = findNativeElement(fixture, `.markdown-run-button`);
148+
runButtonElement.click();
149+
150+
expect(navigateToDatasetViewSpy).toHaveBeenCalledWith(
151+
jasmine.objectContaining({ tab: DatasetViewTypeEnum.Data }),
152+
);
153+
});
117154
});

src/app/dataset-view/additional-components/overview-component/components/readme-section/readme-section.component.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { NavigationService } from "./../../../../../services/navigation.service";
12
import {
3+
AfterViewChecked,
24
ChangeDetectionStrategy,
35
Component,
46
EventEmitter,
@@ -15,14 +17,15 @@ import { EditMode } from "./readme-section.types";
1517
import { DatasetCommitService } from "../../services/dataset-commit.service";
1618
import { LoggedUserService } from "src/app/auth/logged-user.service";
1719
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
20+
import { DatasetViewTypeEnum } from "src/app/dataset-view/dataset-view.interface";
1821

1922
@Component({
2023
selector: "app-readme-section",
2124
templateUrl: "./readme-section.component.html",
2225
styleUrls: ["./readme-section.component.scss"],
2326
changeDetection: ChangeDetectionStrategy.OnPush,
2427
})
25-
export class ReadmeSectionComponent extends BaseComponent implements OnChanges {
28+
export class ReadmeSectionComponent extends BaseComponent implements OnChanges, AfterViewChecked {
2629
@Input({ required: true }) public datasetBasics: DatasetBasicsFragment;
2730
@Input({ required: true }) public currentReadme?: MaybeNull<string>;
2831
@Input({ required: true }) public editingInProgress = false;
@@ -37,6 +40,7 @@ export class ReadmeSectionComponent extends BaseComponent implements OnChanges {
3740
return this.currentReadme !== this.readmeState;
3841
}
3942

43+
private navigationService = inject(NavigationService);
4044
private datasetCommitService = inject(DatasetCommitService);
4145
private loggedUserService = inject(LoggedUserService);
4246

@@ -47,6 +51,10 @@ export class ReadmeSectionComponent extends BaseComponent implements OnChanges {
4751
}
4852
}
4953

54+
ngAfterViewChecked(): void {
55+
this.addDynamicRunButton();
56+
}
57+
5058
public get isEditView(): boolean {
5159
return this.viewMode === EditMode.Edit;
5260
}
@@ -88,4 +96,37 @@ export class ReadmeSectionComponent extends BaseComponent implements OnChanges {
8896
this.editingInProgress = false;
8997
this.editViewShowEmitter.emit(this.editingInProgress);
9098
}
99+
100+
private addDynamicRunButton(): void {
101+
if (this.readmeState) {
102+
// Find all sql queries between ```sql and ```
103+
const sqlQueries = this.readmeState.match(/(?<=```sql\s+).*?(?=\s+```)/gs);
104+
const containerRunButtonElement: HTMLCollectionOf<Element> =
105+
document.getElementsByClassName("container-run-button");
106+
107+
if (sqlQueries?.length && !containerRunButtonElement.length) {
108+
const preElements: NodeListOf<Element> = document.querySelectorAll("pre.language-sql");
109+
preElements.forEach((preElement: Element, index: number) => {
110+
const divElement: HTMLDivElement = document.createElement("div");
111+
divElement.classList.add("container-run-button");
112+
divElement.style.position = "absolute";
113+
divElement.style.top = "7px";
114+
divElement.style.right = "65px";
115+
const buttonElement: HTMLButtonElement = document.createElement("button");
116+
buttonElement.innerHTML = "Run";
117+
buttonElement.classList.add("markdown-run-button");
118+
buttonElement.addEventListener("click", () => {
119+
this.navigationService.navigateToDatasetView({
120+
accountName: this.datasetBasics.owner.accountName,
121+
datasetName: this.datasetBasics.name,
122+
tab: DatasetViewTypeEnum.Data,
123+
sqlQuery: sqlQueries[index],
124+
});
125+
});
126+
divElement.appendChild(buttonElement);
127+
preElement.after(divElement);
128+
});
129+
}
130+
}
131+
}
91132
}

src/app/dataset-view/dataset.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export class DatasetComponent extends BaseDatasetDataComponent implements OnInit
4848
.subscribe(() => {
4949
this.initDatasetViewByType(this.getDatasetInfoFromUrl(), this.getCurrentPageFromUrl());
5050
this.requestMainDataIfChanged();
51+
this.cdr.detectChanges();
5152
});
5253
this.datasetService.datasetChanges
5354
.pipe(takeUntilDestroyed(this.destroyRef))

src/app/interface/navigation.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface DatasetNavigationParams {
55
page?: number;
66
section?: string;
77
state?: object;
8+
sqlQuery?: string;
89
}
910
export interface DatasetInfo {
1011
accountName: string;

src/app/services/navigation.service.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ describe("NavigationService", () => {
167167
const routerSpy = spyOn(router, "navigate").and.resolveTo(true);
168168
service.navigateToDatasetView(mockParams);
169169
expect(routerSpy).toHaveBeenCalledWith([mockParams.accountName, mockParams.datasetName], {
170-
queryParams: { tab: mockParams.tab, page: mockParams.page, section: undefined },
170+
queryParams: { tab: mockParams.tab, page: mockParams.page, section: undefined, sqlQuery: undefined },
171171
state: undefined,
172172
});
173173
});

src/app/services/navigation.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export class NavigationService {
9090
queryParams:
9191
params.page === 1
9292
? { tab: params.tab }
93-
: { tab: params.tab, section: params.section, page: params.page },
93+
: { tab: params.tab, section: params.section, page: params.page, sqlQuery: params.sqlQuery },
9494
state: params.state,
9595
}),
9696
);

0 commit comments

Comments
 (0)