diff --git a/ui-ngx/src/app/modules/home/components/home-components.module.ts b/ui-ngx/src/app/modules/home/components/home-components.module.ts
index a6c0310720..7b4f3c6c54 100644
--- a/ui-ngx/src/app/modules/home/components/home-components.module.ts
+++ b/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,
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html
index 0174208c41..a075cf5847 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html
+++ b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.html
@@ -53,12 +53,13 @@
}
@case ('instruction') {
-
-
-
-
+
+
}
@case ('form') {
@@ -277,9 +278,7 @@
@for (ws of wizardSteps; track ws.label) {
-
-
-
+
}
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss
index 64edf48f95..aff20e4d58 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.scss
+++ b/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 {
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts
index a5f3867046..835c1c8cb4 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/device-install-dialog/device-install-dialog.component.ts
+++ b/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[] = [IotHubItemLinkModule];
-
loading = true;
packageInfo: DevicePackageInfo;
zipFiles = new Map();
@@ -122,6 +117,8 @@ export class TbDeviceInstallDialogComponent extends DialogComponent = {};
gatewayDockerComposeContent: string | null = null;
+ resolveMarkdownVariable: (key: string) => string | undefined = this._resolveMarkdownVariable.bind(this);
+
constructor(
protected store: Store,
protected router: Router,
@@ -239,7 +236,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent {
- img.addEventListener('click', () => {
- img.classList.toggle('tb-gallery-img-expanded');
- });
- });
}
resolveImagePath(path: string): string {
@@ -473,70 +463,44 @@ export class TbDeviceInstallDialogComponent extends DialogComponent {
- 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 '⬇ Download docker-compose.yml';
- }
- // IoT Hub item link card: ${item-link:}
- 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 = { note: 'info_outline', warn: 'warning_amber', error: 'error_outline' };
- return `${icons[type]}${text}
`;
- }
- // 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) => `
`)
- .join('');
- return `${images}
`;
- }
- 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 ? `` : 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 '⬇ Download docker-compose.yml';
+ }
+ return undefined;
}
// --- Private ---
@@ -582,7 +546,7 @@ export class TbDeviceInstallDialogComponent extends DialogComponent
@if (readmeContent) {
-
+
}
@if (shouldShowChangelog()) {
{{ 'iot-hub.changelog' | translate }}
-
+
}
}
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts
index fb1632f1dd..c770045ddc 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-item-detail-dialog.component.ts
+++ b/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 {
readonly ItemType = ItemType;
- readonly itemLinkCompileModules: Type[] = [IotHubItemLinkModule];
item: MpItemVersionView;
mode: IotHubItemDetailDialogMode;
showCreator: boolean;
@@ -310,24 +306,8 @@ export class TbIotHubItemDetailDialogComponent extends DialogComponent 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' }
- );
- }
}
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.html
index 83b8f82b59..a7c2d3bd04 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.html
+++ b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.html
@@ -15,9 +15,11 @@
limitations under the License.
-->
-
+ [fallbackToPlainMarkdown]="fallbackToPlainMarkdown"
+ (ready)="onReady()">
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss
index 68278d2fc6..2c4ba9918d 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.scss
+++ b/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%;
+ }
}
}
}
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts
index b8e9d4139c..445c2bc326 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.component.ts
+++ b/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 | 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();
+
+ additionalStyles: string[] = [];
+
+ parsedData: string;
+
readonly itemLinkCompileModules: Type[] = [IotHubItemLinkModule];
+
+ constructor(
+ private iotHubApiService: IotHubApiService,
+ private elementRef: ElementRef
+ ) {}
+
+ 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 ? `` : 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 = { note: 'info_outline', warn: 'warning_amber', error: 'error_outline' };
+ return `${icons[type]}${text}
`;
+ }
+ // 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) => `
`)
+ .join('');
+ return `${images}
`;
+ }
+
+ // Special variables
+ const res = this.onResolveVariable(key);
+ if (res) {
+ return res;
+ }
+ return '${' + key + '}';
+ });
+ }
}
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.utils.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.utils.ts
index b0a87fb11e..30872c3b20 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-markdown.utils.ts
+++ b/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 `` +
+ `${icon}${safeText}`;
+}
+
+function escapeHtml(value: string): string {
+ return value.replace(/[&<>]/g, ch => ch === '&' ? '&' : ch === '<' ? '<' : '>');
+}
+
+function escapeHtmlAttr(value: string): string {
+ return value.replace(/[&<>"']/g, ch => {
+ switch (ch) {
+ case '&': return '&';
+ case '<': return '<';
+ case '>': return '>';
+ case '"': return '"';
+ default: return ''';
+ }
+ });
+}
+
diff --git a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts
index 71f7b93716..e11528aa6b 100644
--- a/ui-ngx/src/app/modules/home/components/iot-hub/iot-hub-update-dialog.component.ts
+++ b/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;
diff --git a/ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.html b/ui-ngx/src/app/modules/home/components/iot-hub/solution-install-dialog.component.html
similarity index 100%
rename from ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.html
rename to ui-ngx/src/app/modules/home/components/iot-hub/solution-install-dialog.component.html
diff --git a/ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.scss b/ui-ngx/src/app/modules/home/components/iot-hub/solution-install-dialog.component.scss
similarity index 100%
rename from ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.scss
rename to ui-ngx/src/app/modules/home/components/iot-hub/solution-install-dialog.component.scss
diff --git a/ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.ts b/ui-ngx/src/app/modules/home/components/iot-hub/solution-install-dialog.component.ts
similarity index 100%
rename from ui-ngx/src/app/modules/home/components/solution/solution-install-dialog.component.ts
rename to ui-ngx/src/app/modules/home/components/iot-hub/solution-install-dialog.component.ts