Browse Source

refactor(iot-hub): co-locate solution install dialog with iot-hub module

- Move solution-install-dialog.component.{ts,html,scss} from
  home/components/solution/ into home/components/iot-hub/, drop the
  now-empty solution folder.
- Register SolutionInstallDialogComponent in IotHubComponentsModule
  (declaration + export) and remove its declaration / export /
  import from HomeComponentsModule. HomeComponentsModule already
  imports IotHubComponentsModule, so consumers continue to resolve
  the component without changes.
- Update import paths in iot-hub-install-dialog,
  iot-hub-update-dialog, iot-hub-item-detail-dialog (and the
  modified home-components module) to the new
  @home/components/iot-hub/solution-install-dialog.component path.
- Pick up incidental work-in-progress edits in the iot-hub markdown
  utils / template / dialogs touched alongside this move.
pull/15615/head
Igor Kulikov 3 months ago
parent
commit
f3b5dbe233
  1. 3
      ui-ngx/src/app/modules/home/components/home-components.module.ts
  2. 17
      ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html
  3. 202
      ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss
  4. 108
      ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts
  5. 7
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts
  6. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts
  7. 4
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html
  8. 24
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts
  9. 6
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.html
  10. 140
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss
  11. 158
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts
  12. 47
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.utils.ts
  13. 2
      ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts
  14. 0
      ui-ngx/src/app/modules/home/components/iot-hub/solution-install-dialog.component.html
  15. 0
      ui-ngx/src/app/modules/home/components/iot-hub/solution-install-dialog.component.scss
  16. 0
      ui-ngx/src/app/modules/home/components/iot-hub/solution-install-dialog.component.ts

3
ui-ngx/src/app/modules/home/components/home-components.module.ts

@ -207,7 +207,6 @@ import { CheckConnectivityDialogComponent } from '@home/components/ai-model/chec
import { AIModelDialogComponent } from '@home/components/ai-model/ai-model-dialog.component';
import { ResourcesDialogComponent } from "@home/components/resources/resources-dialog.component";
import { ResourcesLibraryComponent } from "@home/components/resources/resources-library.component";
import { SolutionInstallDialogComponent } from '@home/components/solution/solution-install-dialog.component';
@NgModule({
declarations:
@ -362,7 +361,6 @@ import { SolutionInstallDialogComponent } from '@home/components/solution/soluti
AIModelDialogComponent,
ResourcesDialogComponent,
ResourcesLibraryComponent,
SolutionInstallDialogComponent,
],
imports: [
CommonModule,
@ -513,7 +511,6 @@ import { SolutionInstallDialogComponent } from '@home/components/solution/soluti
AIModelDialogComponent,
ResourcesDialogComponent,
ResourcesLibraryComponent,
SolutionInstallDialogComponent,
],
providers: [
WidgetComponentService,

17
ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html

@ -53,12 +53,13 @@
}
@case ('instruction') {
<div class="tb-device-install-instruction" #instructionContainer>
<tb-markdown [data]="ws.markdown"
[additionalCompileModules]="itemLinkCompileModules"
(ready)="onMarkdownReady(instructionContainer)">
</tb-markdown>
</div>
<tb-iot-hub-markdown [data]="ws.markdown"
[packageInfo]="packageInfo"
[imageMap]="zipImages"
[onResolveVariable]="resolveMarkdownVariable"
[codeBlockMaxHeightPx]="400"
(ready)="onMarkdownReady($event)">
</tb-iot-hub-markdown>
}
@case ('form') {
@ -277,9 +278,7 @@
@for (ws of wizardSteps; track ws.label) {
<mat-tab [label]="ws.label">
<ng-template matTabContent>
<div class="tb-tab-content">
<ng-container *ngTemplateOutlet="stepContent; context: { $implicit: ws }"></ng-container>
</div>
<ng-container *ngTemplateOutlet="stepContent; context: { $implicit: ws }"></ng-container>
</ng-template>
</mat-tab>
}

202
ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss

@ -28,6 +28,9 @@
height: 1000px;
max-width: 100%;
}
@media #{$mat-gt-md} {
width: 1000px;
}
}
:host {
@ -197,205 +200,6 @@
}
}
.tb-tab-content {
padding: 16px 24px;
}
// Instruction view
.tb-device-install-instruction {
font-size: 14px;
line-height: 24px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
::ng-deep tb-markdown .tb-markdown-view {
padding: 16px 24px 24px;
h1, h2, h3, h4, h5, h6 {
font-size: 20px;
font-weight: 600;
line-height: 24px;
letter-spacing: 0.1px;
color: rgba(0, 0, 0, 0.76);
margin: 0;
padding: 0;
}
> h1, > h2, > h3, > h4, > h5, > h6 {
padding: 0;
margin-top: 20px;
}
> :first-child {
margin-top: 0;
}
h1 {
font-size: 24px;
line-height: 32px;
padding-right: 0;
}
p {
font-size: 14px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
margin: 0;
}
> p, > div {
padding-left: 0;
padding-right: 0;
}
p + p {
margin-top: 8px;
}
h1 + p, h2 + p, h3 + p,
h1 + ul, h2 + ul, h3 + ul,
h1 + ol, h2 + ol, h3 + ol {
margin-top: 8px;
}
ul, ol {
padding-left: 21px;
padding-right: 0;
margin: 0;
font-size: 14px;
line-height: 24px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
+ h1, + h2, + h3, + h4, + h5, + h6 {
padding-top: 0;
margin-top: 20px;
}
}
li {
padding-bottom: 0;
margin-bottom: 0;
line-height: 24px;
}
code:not([class*=language-]) {
font-size: 14px;
}
}
::ng-deep {
.tb-download-btn {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 16px;
border: 1px solid #305680;
border-radius: 4px;
color: #305680;
font-size: 14px;
font-weight: 500;
text-decoration: none;
cursor: pointer;
&:hover {
background: rgba(48, 86, 128, 0.08);
}
}
.tb-doc-link-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
margin: 4px 8px 4px 0;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
background: #fff;
color: rgba(0, 0, 0, 0.76);
font-size: 14px;
font-weight: 500;
line-height: 20px;
text-decoration: none;
cursor: pointer;
i.material-icons {
font-size: 18px;
line-height: 18px;
color: rgba(0, 0, 0, 0.54);
}
&:hover {
background: rgba(0, 0, 0, 0.04);
border-color: rgba(0, 0, 0, 0.24);
}
}
.tb-callout {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 16px;
border-radius: 4px;
border: 1px solid;
margin: 12px 0;
font-size: 14px;
line-height: 20px;
.tb-callout-icon {
font-size: 20px;
flex-shrink: 0;
}
&.tb-callout-note {
background: #e3f2fd;
border-color: #90caf9;
color: #1565c0;
}
&.tb-callout-warn {
background: #fff8e1;
border-color: #ffe082;
color: #f57f17;
}
&.tb-callout-error {
background: #fce4ec;
border-color: #ef9a9a;
color: #c62828;
}
}
.tb-gallery {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 12px 0;
}
.tb-gallery-img {
max-height: 160px;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.12);
cursor: pointer;
transition: max-height 0.2s ease;
object-fit: contain;
&:hover {
border-color: rgba(0, 0, 0, 0.24);
}
&.tb-gallery-img-expanded {
max-height: none;
max-width: 100%;
}
}
}
}
// Form view
.tb-device-install-form {
mat-form-field {

108
ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts

@ -39,8 +39,6 @@ import { AttributeService } from '@core/http/attribute.service';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { EntityId } from '@shared/models/id/entity-id';
import { generateSecret } from '@core/utils';
import { IotHubItemLinkModule } from '../iot-hub-item-link-card/iot-hub-item-link.module';
import { ITEM_LINK_KEY_REGEX, itemLinkCardTag } from '../iot-hub-markdown.utils';
import {
installMethodIcons as INSTALL_METHOD_ICONS,
installMethodLabels as INSTALL_METHOD_LABELS,
@ -53,7 +51,6 @@ import {
FormFieldDefinition,
FormFieldType,
InstallStepType,
resolveDocLinkPlaceholders,
stepTypeAliasMap
} from '@shared/models/iot-hub/device-package.models';
@ -96,8 +93,6 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
@ViewChild('installStepper', {static: false}) stepper: MatStepper;
readonly itemLinkCompileModules: Type<any>[] = [IotHubItemLinkModule];
loading = true;
packageInfo: DevicePackageInfo;
zipFiles = new Map<string, string>();
@ -122,6 +117,8 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
transportVars: Record<string, string> = {};
gatewayDockerComposeContent: string | null = null;
resolveMarkdownVariable: (key: string) => string | undefined = this._resolveMarkdownVariable.bind(this);
constructor(
protected store: Store<AppState>,
protected router: Router,
@ -239,7 +236,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
if (!ws) return;
if (ws.type === 'instruction' && !ws.markdown) {
const raw = this.zipFiles.get(ws.rawSteps[0].file) || '';
ws.markdown = this.resolveImages(this.resolveVariables(raw));
ws.markdown = raw;
} else if (ws.type === 'progress' && !ws.progressDone) {
this.showCompletedEntitySteps(ws);
}
@ -450,13 +447,6 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
}
});
});
// Gallery image click-to-expand
const galleryImages = container.querySelectorAll('.tb-gallery-img');
galleryImages.forEach(img => {
img.addEventListener('click', () => {
img.classList.toggle('tb-gallery-img-expanded');
});
});
}
resolveImagePath(path: string): string {
@ -473,70 +463,44 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
// --- Variable Resolution ---
resolveVariables(content: string): string {
if (this.packageInfo) {
content = resolveDocLinkPlaceholders(
content,
this.packageInfo.name || '',
{ productURL: this.packageInfo.productURL, datasheetURL: this.packageInfo.datasheetURL },
{ productPage: 'Product page', datasheet: 'Datasheet' }
);
}
return content.replace(/\$\{([^}]+)}/g, (_match, key) => {
if (key in this.formValues) {
return String(this.formValues[key]);
}
if (key in this.transportVars) {
return this.transportVars[key];
}
// Special action placeholders
if (key === 'gateway.downloadButton') {
return '<a href="#" data-action="download-gateway-docker-compose" class="tb-download-btn">⬇ Download docker-compose.yml</a>';
}
// IoT Hub item link card: ${item-link:<uuid>}
const itemLinkMatch = key.match(ITEM_LINK_KEY_REGEX);
if (itemLinkMatch) {
return itemLinkCardTag(itemLinkMatch[1]);
}
// Callout boxes: ${note(...)}, ${warn(...)}, ${error(...)}
const calloutMatch = key.match(/^(note|warn|error)\((.+)\)$/s);
if (calloutMatch) {
const type = calloutMatch[1];
const text = calloutMatch[2];
const icons: Record<string, string> = { note: 'info_outline', warn: 'warning_amber', error: 'error_outline' };
return `<div class="tb-callout tb-callout-${type}"><i class="material-icons tb-callout-icon">${icons[type]}</i><span class="tb-callout-text">${text}</span></div>`;
}
// Image gallery: ${images.gallery(path1,path2,path3)}
const galleryMatch = key.match(/^images\.gallery\((.+)\)$/);
if (galleryMatch) {
const paths = galleryMatch[1].split(',').map((p: string) => p.trim());
const images = paths
.map((p: string) => this.zipImages.get(p))
.filter((src: string | undefined) => !!src)
.map((src: string) => `<img src="${src}" alt="" class="tb-gallery-img" />`)
.join('');
return `<div class="tb-gallery">${images}</div>`;
}
const dotIdx = key.indexOf('.');
if (dotIdx > 0) {
const alias = key.substring(0, dotIdx);
const prop = key.substring(dotIdx + 1);
const output = this.entityOutputs.get(alias);
if (output && prop in output) {
return String((output as any)[prop]);
}
const res = this.resolveVariable(key);
if (res) {
return res;
}
return '${' + key + '}';
});
}
resolveImages(content: string): string {
return content.replace(/!\[([^\]]*)]\(([^)]+)\)/g, (match, alt, path) => {
if (path.startsWith('data:') || path.startsWith('http')) {
return match;
resolveVariable(key: string): string | undefined {
if (key in this.formValues) {
return String(this.formValues[key]);
}
if (key in this.transportVars) {
return this.transportVars[key];
}
const dotIdx = key.indexOf('.');
if (dotIdx > 0) {
const alias = key.substring(0, dotIdx);
const prop = key.substring(dotIdx + 1);
const output = this.entityOutputs.get(alias);
if (output && prop in output) {
return String((output as any)[prop]);
}
const dataUri = this.zipImages.get(path);
return dataUri ? `![${alt}](${dataUri})` : match;
});
}
return undefined;
}
private _resolveMarkdownVariable(key: string): string | undefined {
const res = this.resolveVariable(key);
if (res) {
return res;
}
// Special action placeholders
if (key === 'gateway.downloadButton') {
return '<a href="#" data-action="download-gateway-docker-compose" mat-stroked-button color="primary">⬇ Download docker-compose.yml</a>';
}
return undefined;
}
// --- Private ---
@ -582,7 +546,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
for (const ws of this.wizardSteps) {
if (ws.type === 'instruction') {
const raw = this.zipFiles.get(ws.rawSteps[0].file) || '';
ws.markdown = this.resolveImages(this.resolveVariables(raw));
ws.markdown = raw;
} else if (ws.type === 'progress') {
this.showCompletedEntitySteps(ws);
}
@ -725,7 +689,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent<TbDeviceInst
}
if (step.type === 'instruction') {
const raw = this.zipFiles.get(step.rawSteps[0].file) || '';
step.markdown = this.resolveImages(this.resolveVariables(raw));
step.markdown = raw;
} else if (step.type === 'progress' && !step.progressDone) {
if (this.reviewMode) {
this.showCompletedEntitySteps(step);

7
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-components.module.ts

@ -31,6 +31,7 @@ import { TbIotHubInstalledItemsTableComponent } from './iot-hub-installed-items-
import { TbIotHubInstalledItemsDialogComponent } from './iot-hub-installed-items-dialog.component';
import { TbPeConnectivityMethodPromptComponent } from './pe-connectivity-method-prompt.component';
import { TbIotHubMarkdownComponent } from './iot-hub-markdown.component';
import { SolutionInstallDialogComponent } from './solution-install-dialog.component';
import { IotHubActionsService } from './iot-hub-actions.service';
import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link.module';
@ -49,7 +50,8 @@ import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link
TbIotHubInstalledItemsTableComponent,
TbIotHubInstalledItemsDialogComponent,
TbPeConnectivityMethodPromptComponent,
TbIotHubMarkdownComponent
TbIotHubMarkdownComponent,
SolutionInstallDialogComponent
],
imports: [
CommonModule,
@ -73,7 +75,8 @@ import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link
TbIotHubInstalledItemsTableComponent,
TbIotHubInstalledItemsDialogComponent,
TbPeConnectivityMethodPromptComponent,
TbIotHubMarkdownComponent
TbIotHubMarkdownComponent,
SolutionInstallDialogComponent
]
})
export class IotHubComponentsModule { }

2
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-install-dialog.component.ts

@ -28,7 +28,7 @@ import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models';
import { EntityId } from '@shared/models/id/entity-id';
import { resolveEntityDetailsUrl } from './iot-hub-components.models';
import { SolutionInstallDialogComponent } from '@home/components/solution/solution-install-dialog.component';
import { SolutionInstallDialogComponent } from '@home/components/iot-hub/solution-install-dialog.component';
export interface IotHubInstallDialogData {
item: MpItemVersionView;

4
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.html

@ -299,11 +299,11 @@
@if (readmeContent || shouldShowChangelog()) {
<div class="dlg-readme">
@if (readmeContent) {
<tb-iot-hub-markdown [data]="readmeContent"></tb-iot-hub-markdown>
<tb-iot-hub-markdown [data]="readmeContent" [item]="item"></tb-iot-hub-markdown>
}
@if (shouldShowChangelog()) {
<h3>{{ 'iot-hub.changelog' | translate }}</h3>
<tb-iot-hub-markdown [data]="item.changelog"></tb-iot-hub-markdown>
<tb-iot-hub-markdown [data]="item.changelog" [item]="item"></tb-iot-hub-markdown>
}
</div>
}

24
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts

@ -24,15 +24,12 @@ import { MpItemVersionView, cfTypeTranslations, cfTypeIcons, ruleChainTypeTransl
import { ItemType, itemTypeTranslations } from '@shared/models/iot-hub/iot-hub-item.models';
import { IotHubInstalledItem } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { resolveDocLinkPlaceholders } from '@shared/models/iot-hub/device-package.models';
import { TranslateService } from '@ngx-translate/core';
import { EntityType } from '@shared/models/entity-type.models';
import { getEntityDetailsPageURL } from '@core/utils';
import { SolutionInstallDialogComponent } from '@home/components/solution/solution-install-dialog.component';
import { SolutionInstallDialogComponent } from '@home/components/iot-hub/solution-install-dialog.component';
import { SolutionTemplateInstalledItemDescriptor } from '@shared/models/iot-hub/iot-hub-installed-item.models';
import { IotHubActionsService } from '@home/components/iot-hub/iot-hub-actions.service';
import { replaceItemLinkPlaceholders } from './iot-hub-markdown.utils';
import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link.module';
export type IotHubItemDetailDialogMode = 'default' | 'add';
@ -54,7 +51,6 @@ export interface IotHubItemDetailDialogData {
export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubItemDetailDialogComponent> {
readonly ItemType = ItemType;
readonly itemLinkCompileModules: Type<any>[] = [IotHubItemLinkModule];
item: MpItemVersionView;
mode: IotHubItemDetailDialogMode;
showCreator: boolean;
@ -310,24 +306,8 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent<TbIotHubI
private loadReadme(): void {
const versionId = this.item.id as string;
this.iotHubApiService.getVersionReadme(versionId, { ignoreLoading: true }).subscribe(
content => this.readmeContent = replaceItemLinkPlaceholders(
this.resolveDocLinks(this.prefixResourceUrls(content || ''))
)
content => this.readmeContent = content
);
}
private prefixResourceUrls(markdown: string): string {
const baseUrl = this.iotHubApiService.baseUrl;
return markdown.replace(/([("])(\/api\/resources\/[^)"]*)/g, `$1${baseUrl}$2`);
}
private resolveDocLinks(markdown: string): string {
const dd = this.item.dataDescriptor;
return resolveDocLinkPlaceholders(
markdown,
this.item.name || dd?.name || '',
{ productURL: dd?.productURL, datasheetURL: dd?.datasheetURL },
{ productPage: 'Product page', datasheet: 'Datasheet' }
);
}
}

6
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.html

@ -15,9 +15,11 @@
limitations under the License.
-->
<tb-markdown [data]="data"
<tb-markdown [data]="parsedData"
[applyDefaultMarkdownStyle]="false"
[additionalStyles]="additionalStyles"
[additionalCompileModules]="itemLinkCompileModules"
[lineNumbers]="lineNumbers"
[fallbackToPlainMarkdown]="fallbackToPlainMarkdown">
[fallbackToPlainMarkdown]="fallbackToPlainMarkdown"
(ready)="onReady()">
</tb-markdown>

140
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss

@ -14,14 +14,28 @@
* limitations under the License.
*/
// Shared markdown styles for IoT Hub readme/description content.
// Originally in iot-hub-item-detail-dialog.component.scss (.dlg-readme).
$text-color: rgba(0,0,0,0.76);
$link-color: #2a7dec;
$copy-button-color: #2a7dec;
$code-block-border-color: rgba(42, 125, 236, .2);
$code-default-color: #212529;
$code-text-color: #eb5757;
$code-keyword-color: #2a7dec;
$table-border-color: rgba(42, 125, 236, .2);
$table-header-background-color: #f9fbff;
$table-header-text-color: rgba(33, 37, 41, .6);
$table-text-color: rgba(15, 22, 29, 0.8);
:host {
display: block;
font-size: 14px;
line-height: 24px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
color: $text-color;
--mat-button-outlined-label-text-color: #{$text-color};
::ng-deep tb-markdown .tb-markdown-view {
padding: 16px 24px 24px;
@ -40,7 +54,7 @@
font-weight: 600;
line-height: 24px;
letter-spacing: 0.1px;
color: rgba(0, 0, 0, 0.76);
color: $text-color;
margin: 0;
padding: 0;
}
@ -62,7 +76,7 @@
font-weight: 400;
line-height: 24px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
color: $text-color;
margin: 0;
}
@ -89,7 +103,7 @@
font-size: 14px;
line-height: 24px;
letter-spacing: 0.2px;
color: rgba(0, 0, 0, 0.76);
color: $text-color;
+ h1, + h2, + h3, + h4, + h5, + h6 {
padding-top: 0;
@ -108,35 +122,30 @@
line-height: 24px;
}
// Images ported from markdown.component.scss.
img {
max-width: 100%;
}
// Links ported from markdown.component.scss.
a {
a:not(.mdc-button) {
font-weight: 500;
color: #2a7dec;
color: $link-color;
text-decoration: none;
border: none;
&:hover {
color: #2a7dec;
color: $link-color;
text-decoration: underline;
border: none;
}
}
// Inline code ported from markdown.component.scss.
code:not([class*=language-]) {
color: #eb5757;
color: $code-text-color;
font-family: monospace;
font-size: 14px;
}
// Code-wrapper + clipboard button ported from markdown.component.scss
// so the inline copy affordance and hover state work without
// depending on the default markdown styles.
// Code-wrapper + clipboard button
div.code-wrapper {
position: relative;
@ -160,7 +169,7 @@
padding: 6px 8px 0;
top: 1px;
transition: .2s;
color: #2a7dec;
color: $copy-button-color;
background: rgba(255, 255, 255, .85);
backdrop-filter: blur(4px);
opacity: 0;
@ -191,7 +200,7 @@
cursor: pointer;
pre[class*="language-"] {
border: solid 1px #2a7dec;
border: solid 1px $copy-button-color;
}
button.clipboard-btn {
@ -218,25 +227,25 @@
}
}
// Code blocks (Prism) ported from markdown.component.scss.
// Code blocks (Prism)
pre[class*="language-"] {
font-size: 14px;
border: 1px solid rgba(42, 125, 236, .2);
border: 1px solid $code-block-border-color;
border-radius: 4px;
background: 0 0;
padding: 8px 16px;
color: #212529;
color: $code-default-color;
.token.atrule, .token.attr-value, .token.keyword {
color: #2a7dec;
color: $code-keyword-color;
}
.token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted {
color: #eb5757;
color: $code-text-color;
}
.token.punctuation {
color: #212529;
color: $code-default-color;
}
&.line-numbers {
@ -268,11 +277,10 @@
}
}
// Tables ported from markdown.component.scss since
// applyDefaultMarkdownStyle is false on tb-markdown.
// Tables
> table {
width: 100%;
border: 1px solid rgba(42, 125, 236, .2);
border: 1px solid $table-border-color;
border-radius: 4px;
border-collapse: unset;
border-spacing: 0;
@ -286,11 +294,11 @@
}
> thead {
background-color: #f9fbff;
color: rgba(33, 37, 41, .6);
background-color: $table-header-background-color;
color: $table-header-text-color;
> tr > th {
border-bottom: 1px solid rgba(42, 125, 236, .2);
border-bottom: 1px solid $table-border-color;
font-size: 14px;
padding: 8px 12px;
text-align: left;
@ -307,7 +315,7 @@
> tbody {
> tr:not(:last-child) > td {
border-bottom: 1px solid rgba(42, 125, 236, .2);
border-bottom: 1px solid $table-border-color;
}
> tr > td {
@ -315,7 +323,7 @@
padding: 8px 12px;
text-align: left;
margin: 0;
color: rgba(15, 22, 29, 0.8);
color: $table-text-color;
@media screen and (max-width: 400px) {
font-size: 12px;
padding: 8px 4px;
@ -347,32 +355,64 @@
}
}
.tb-doc-link-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
margin: 4px 8px 4px 0;
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 6px;
background: #fff;
color: rgba(0, 0, 0, 0.76);
.tb-callout {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 16px;
border-radius: 4px;
border: 1px solid;
margin: 12px 0;
font-size: 14px;
font-weight: 500;
line-height: 20px;
text-decoration: none;
cursor: pointer;
i.material-icons {
font-size: 18px;
line-height: 18px;
color: rgba(0, 0, 0, 0.54);
.tb-callout-icon {
font-size: 20px;
flex-shrink: 0;
}
&.tb-callout-note {
background: #e3f2fd;
border-color: #90caf9;
color: #1565c0;
}
&.tb-callout-warn {
background: #fff8e1;
border-color: #ffe082;
color: #f57f17;
}
&.tb-callout-error {
background: #fce4ec;
border-color: #ef9a9a;
color: #c62828;
}
}
.tb-gallery {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 12px 0;
}
.tb-gallery-img {
max-height: 160px;
border-radius: 4px;
border: 1px solid rgba(0, 0, 0, 0.12);
cursor: pointer;
transition: max-height 0.2s ease;
object-fit: contain;
&:hover {
background: rgba(0, 0, 0, 0.04);
border-color: rgba(0, 0, 0, 0.24);
}
&.tb-gallery-img-expanded {
max-height: none;
max-width: 100%;
}
}
}
}

158
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts

@ -14,9 +14,26 @@
/// limitations under the License.
///
import { Component, Input, Type } from '@angular/core';
import {
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
Type
} from '@angular/core';
import { coerceBoolean } from '@shared/decorators/coercion';
import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link.module';
import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { MpItemVersionView } from '@shared/models/iot-hub/iot-hub-version.models';
import {
replaceItemLinkPlaceholders,
resolveDocLinkPlaceholders
} from '@home/components/iot-hub/iot-hub-markdown.utils';
import { DevicePackageInfo } from '@shared/models/iot-hub/device-package.models';
@Component({
selector: 'tb-iot-hub-markdown',
@ -24,10 +41,18 @@ import { IotHubItemLinkModule } from './iot-hub-item-link-card/iot-hub-item-link
templateUrl: './iot-hub-markdown.component.html',
styleUrls: ['./iot-hub-markdown.component.scss']
})
export class TbIotHubMarkdownComponent {
export class TbIotHubMarkdownComponent implements OnInit, OnChanges {
@Input() data: string | undefined;
@Input() item: MpItemVersionView | undefined;
@Input() packageInfo: DevicePackageInfo | undefined;
@Input() imageMap: Map<string, string> | undefined;
@Input() onResolveVariable: (key: string) => string | undefined = () => undefined;
@Input()
@coerceBoolean()
lineNumbers = false;
@ -36,5 +61,134 @@ export class TbIotHubMarkdownComponent {
@coerceBoolean()
fallbackToPlainMarkdown = false;
@Input()
codeBlockMaxHeightPx: number;
@Output() ready = new EventEmitter<HTMLElement>();
additionalStyles: string[] = [];
parsedData: string;
readonly itemLinkCompileModules: Type<any>[] = [IotHubItemLinkModule];
constructor(
private iotHubApiService: IotHubApiService,
private elementRef: ElementRef<HTMLElement>
) {}
ngOnInit(): void {
if (this.codeBlockMaxHeightPx) {
const codeBlockMaxHeightStyle = `pre[class*="language-"] \n{
max-height: ${this.codeBlockMaxHeightPx}px;\n
}`;
this.additionalStyles.push(codeBlockMaxHeightStyle);
}
}
ngOnChanges(changes: SimpleChanges): void {
for (const propName of Object.keys(changes)) {
const change = changes[propName];
if (propName === 'data' && change.currentValue !== change.previousValue) {
this.parsedData = this.parseData(this.data);
}
}
}
onReady() {
const container = this.elementRef.nativeElement;
// Gallery image click-to-expand
const galleryImages = container.querySelectorAll('.tb-gallery-img');
galleryImages.forEach(img => {
img.addEventListener('click', () => {
img.classList.toggle('tb-gallery-img-expanded');
});
});
this.ready.emit(container);
}
private parseData(content: string | undefined): string {
let parsed = this.prefixResourceUrls(content || '');
parsed = this.resolveDocLinks(parsed);
parsed = replaceItemLinkPlaceholders(parsed);
parsed = this.resolveImages(parsed);
parsed = this.resolveVariables(parsed);
return parsed;
}
private prefixResourceUrls(markdown: string): string {
const baseUrl = this.iotHubApiService.baseUrl;
return markdown.replace(/([("])(\/api\/resources\/[^)"]*)/g, `$1${baseUrl}$2`);
}
private resolveDocLinks(markdown: string): string {
if (this.item || this.packageInfo) {
const dd = this.item ? this.item.dataDescriptor : this.packageInfo;
return resolveDocLinkPlaceholders(
markdown,
this.item?.name || dd?.name || '',
{ productURL: dd?.productURL, datasheetURL: dd?.datasheetURL },
{ productPage: 'Product page', datasheet: 'Datasheet' }
);
} else {
return markdown;
}
}
private resolveImages(content: string): string {
if (this.imageMap) {
return content.replace(/!\[([^\]]*)]\(([^)]+)\)/g, (match, alt, path) => {
if (path.startsWith('data:') || path.startsWith('http')) {
return match;
}
const dataUri = this.imageMap.get(path);
return dataUri ? `![${alt}](${dataUri})` : match;
});
} else {
return content;
}
}
private resolveImage(uri: string): string {
if (this.imageMap && uri) {
if (uri.startsWith('data:') || uri.startsWith('http')) {
return uri;
}
const dataUri = this.imageMap.get(uri);
return dataUri ? dataUri : uri;
} else {
return uri;
}
}
private resolveVariables(content: string): string {
return content.replace(/\$\{([^}]+)}/g, (_match, key) => {
// Callout boxes: ${note(...)}, ${warn(...)}, ${error(...)}
const calloutMatch = key.match(/^(note|warn|error)\((.+)\)$/s);
if (calloutMatch) {
const type = calloutMatch[1];
const text = calloutMatch[2];
const icons: Record<string, string> = { note: 'info_outline', warn: 'warning_amber', error: 'error_outline' };
return `<div class="tb-callout tb-callout-${type}"><i class="material-icons tb-callout-icon">${icons[type]}</i><span class="tb-callout-text">${text}</span></div>`;
}
// Image gallery: ${images.gallery(path1,path2,path3)}
const galleryMatch = key.match(/^images\.gallery\((.+)\)$/);
if (galleryMatch) {
const paths = galleryMatch[1].split(',').map((p: string) => p.trim());
const images = paths
.map((p: string) => this.resolveImage(p))
.filter((src: string | undefined) => !!src)
.map((src: string) => `<img src="${src}" alt="" class="tb-gallery-img" />`)
.join('');
return `<div class="tb-gallery">${images}</div>`;
}
// Special variables
const res = this.onResolveVariable(key);
if (res) {
return res;
}
return '${' + key + '}';
});
}
}

47
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.utils.ts

@ -30,3 +30,50 @@ export function replaceItemLinkPlaceholders(markdown: string): string {
}
return markdown.replace(ITEM_LINK_PLACEHOLDER_REGEX, (_match, uuid) => itemLinkCardTag(uuid));
}
export interface DocLinks {
productURL?: string;
datasheetURL?: string;
}
export interface DocLinkLabels {
productPage: string;
datasheet: string;
}
export function resolveDocLinkPlaceholders(
markdown: string,
name: string,
links: DocLinks,
labels: DocLinkLabels
): string {
return markdown
.replace(/\$\{product\.button}/g, () =>
links.productURL ? buildDocLinkButton(links.productURL, `${name} ${labels.productPage}`, 'open_in_new') : '')
.replace(/\$\{datasheet\.button}/g, () =>
links.datasheetURL ? buildDocLinkButton(links.datasheetURL, `${name} ${labels.datasheet}`, 'description') : '');
}
function buildDocLinkButton(url: string, text: string, icon: string): string {
const safeUrl = escapeHtmlAttr(url);
const safeText = escapeHtml(text);
return `<a href="${safeUrl}" target="_blank" rel="noopener noreferrer" class="mr-2" mat-stroked-button>` +
`<tb-icon matButtonIcon>${icon}</tb-icon><span>${safeText}</span></a>`;
}
function escapeHtml(value: string): string {
return value.replace(/[&<>]/g, ch => ch === '&' ? '&amp;' : ch === '<' ? '&lt;' : '&gt;');
}
function escapeHtmlAttr(value: string): string {
return value.replace(/[&<>"']/g, ch => {
switch (ch) {
case '&': return '&amp;';
case '<': return '&lt;';
case '>': return '&gt;';
case '"': return '&quot;';
default: return '&#39;';
}
});
}

2
ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts

@ -26,7 +26,7 @@ import { IotHubApiService } from '@core/http/iot-hub-api.service';
import { DialogService } from '@core/services/dialog.service';
import { TranslateService } from '@ngx-translate/core';
import { resolveEntityDetailsUrl } from './iot-hub-components.models';
import { SolutionInstallDialogComponent } from '@home/components/solution/solution-install-dialog.component';
import { SolutionInstallDialogComponent } from '@home/components/iot-hub/solution-install-dialog.component';
export interface IotHubUpdateDialogData {
installedItemId: string;

0
ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.html → ui-ngx/src/app/modules/home/components/iot-hub/solution-install-dialog.component.html

0
ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.scss → ui-ngx/src/app/modules/home/components/iot-hub/solution-install-dialog.component.scss

0
ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.ts → ui-ngx/src/app/modules/home/components/iot-hub/solution-install-dialog.component.ts

Loading…
Cancel
Save