systemlink-cli 1.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slcli/__init__.py +1 -0
- slcli/__main__.py +23 -0
- slcli/_version.py +4 -0
- slcli/asset_click.py +1289 -0
- slcli/cli_formatters.py +218 -0
- slcli/cli_utils.py +504 -0
- slcli/comment_click.py +602 -0
- slcli/completion_click.py +418 -0
- slcli/config.py +81 -0
- slcli/config_click.py +498 -0
- slcli/dff_click.py +979 -0
- slcli/dff_decorators.py +24 -0
- slcli/example_click.py +404 -0
- slcli/example_loader.py +274 -0
- slcli/example_provisioner.py +2777 -0
- slcli/examples/README.md +134 -0
- slcli/examples/_schema/schema-v1.0.json +169 -0
- slcli/examples/demo-complete-workflow/README.md +323 -0
- slcli/examples/demo-complete-workflow/config.yaml +638 -0
- slcli/examples/demo-test-plans/README.md +132 -0
- slcli/examples/demo-test-plans/config.yaml +154 -0
- slcli/examples/exercise-5-1-parametric-insights/README.md +101 -0
- slcli/examples/exercise-5-1-parametric-insights/config.yaml +1589 -0
- slcli/examples/exercise-7-1-test-plans/README.md +93 -0
- slcli/examples/exercise-7-1-test-plans/config.yaml +323 -0
- slcli/examples/spec-compliance-notebooks/README.md +140 -0
- slcli/examples/spec-compliance-notebooks/config.yaml +112 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +1553 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +1577 -0
- slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +912 -0
- slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
- slcli/feed_click.py +892 -0
- slcli/file_click.py +932 -0
- slcli/function_click.py +1400 -0
- slcli/function_templates.py +85 -0
- slcli/main.py +406 -0
- slcli/mcp_click.py +269 -0
- slcli/mcp_server.py +748 -0
- slcli/notebook_click.py +1770 -0
- slcli/platform.py +345 -0
- slcli/policy_click.py +679 -0
- slcli/policy_utils.py +411 -0
- slcli/profiles.py +411 -0
- slcli/response_handlers.py +359 -0
- slcli/routine_click.py +763 -0
- slcli/skill_click.py +253 -0
- slcli/skills/slcli/SKILL.md +713 -0
- slcli/skills/slcli/references/analysis-recipes.md +474 -0
- slcli/skills/slcli/references/filtering.md +236 -0
- slcli/skills/systemlink-webapp/SKILL.md +744 -0
- slcli/skills/systemlink-webapp/references/deployment.md +123 -0
- slcli/skills/systemlink-webapp/references/nimble-angular.md +380 -0
- slcli/skills/systemlink-webapp/references/systemlink-services.md +192 -0
- slcli/ssl_trust.py +93 -0
- slcli/system_click.py +2216 -0
- slcli/table_utils.py +124 -0
- slcli/tag_click.py +794 -0
- slcli/templates_click.py +599 -0
- slcli/testmonitor_click.py +1667 -0
- slcli/universal_handlers.py +305 -0
- slcli/user_click.py +1218 -0
- slcli/utils.py +832 -0
- slcli/web_editor.py +295 -0
- slcli/webapp_click.py +981 -0
- slcli/workflow_preview.py +287 -0
- slcli/workflows_click.py +988 -0
- slcli/workitem_click.py +2258 -0
- slcli/workspace_click.py +576 -0
- slcli/workspace_utils.py +206 -0
- systemlink_cli-1.3.1.dist-info/METADATA +20 -0
- systemlink_cli-1.3.1.dist-info/RECORD +74 -0
- systemlink_cli-1.3.1.dist-info/WHEEL +4 -0
- systemlink_cli-1.3.1.dist-info/entry_points.txt +7 -0
- systemlink_cli-1.3.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: systemlink-webapp
|
|
3
|
+
description: >
|
|
4
|
+
Build, configure, and deploy custom web applications hosted inside NI SystemLink. Use this skill
|
|
5
|
+
whenever a user wants to create a frontend app that runs inside SystemLink (as a webapp), uses the
|
|
6
|
+
Nimble Angular design system (@ni/nimble-angular), calls any SystemLink REST API (tags, test
|
|
7
|
+
results, assets, systems, work items, etc.), or deploys a built web app to SystemLink with slcli.
|
|
8
|
+
Also use it when the user asks about using the @ni/systemlink-clients-ts TypeScript SDK, generating a
|
|
9
|
+
TypeScript API client from a SystemLink OpenAPI spec, troubleshooting CORS or CSP errors on a
|
|
10
|
+
SystemLink-hosted app, or configuring Angular routing for SystemLink's sub-path hosting.
|
|
11
|
+
compatibility:
|
|
12
|
+
models: [claude-sonnet-4-5, claude-opus-4, claude-3-7-sonnet]
|
|
13
|
+
tools: [run_in_terminal, create_file, replace_string_in_file, read_file]
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Building Custom WebApps for SystemLink
|
|
17
|
+
|
|
18
|
+
SystemLink webapps are Angular Single-Page Applications built with the Nimble design system,
|
|
19
|
+
connected to SystemLink REST APIs, and deployed via `slcli webapp publish`. This skill captures
|
|
20
|
+
every gotcha learned from building and deploying real apps.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Step 1: Understand what the user needs
|
|
25
|
+
|
|
26
|
+
Ask before generating any code:
|
|
27
|
+
|
|
28
|
+
1. **Goal** — What should the app show or let the user do? (e.g., "browse live tag values", "review test results", "approve work orders")
|
|
29
|
+
2. **Services** — Which SystemLink services will it call? (tags, test monitor, asset management, systems, work items, feeds, notebooks…)
|
|
30
|
+
3. **Starting point** — Fresh Angular project, or do they have existing code?
|
|
31
|
+
4. **Auth context** — Will the app run on the same SystemLink instance it calls (same-origin cookie auth), or does it need an API key for a remote server?
|
|
32
|
+
|
|
33
|
+
You do NOT need to ask about Angular version or Nimble versions — always use the latest (Angular 19, @ni/nimble-angular latest).
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Step 2: Scaffold the Angular project
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx -y @angular/cli@latest new <app-name> --routing --style=scss --skip-git --no-standalone
|
|
41
|
+
cd <app-name>
|
|
42
|
+
npm install @ni/nimble-angular
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> Use `--no-standalone` to generate an NgModule-based app. SystemLink webapps work best with NgModule because it makes it easy to register all Nimble modules in one place.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Step 3: Add the SystemLink TypeScript SDK
|
|
50
|
+
|
|
51
|
+
**Always use [@ni/systemlink-clients-ts](https://github.com/ni-kismet/nisystemlink-clients-ts) as the first choice** for any SystemLink API call. It ships pre-built, typed SDKs for every major SystemLink service (tags, test monitor, file ingestion, asset management, work items, etc.) so you don't need to generate anything.
|
|
52
|
+
|
|
53
|
+
### Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install @ni/systemlink-clients-ts
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Available services (import paths)
|
|
60
|
+
|
|
61
|
+
| Service | Import path | `baseUrl` (append to `window.location.origin`) |
|
|
62
|
+
|---------|-------------|------------------------------------------------|
|
|
63
|
+
| Feeds | `@ni/systemlink-clients-ts/feeds` | (none — spec paths already include `/nifeed/v1/`) |
|
|
64
|
+
| Tags | `@ni/systemlink-clients-ts/tags` | `+ '/nitag'` |
|
|
65
|
+
| User / Workspaces | `@ni/systemlink-clients-ts/user` | `+ '/niuser/v1'` |
|
|
66
|
+
| Web Application | `@ni/systemlink-clients-ts/web-application` | `+ '/niapp/v1'` |
|
|
67
|
+
| File Ingestion | `@ni/systemlink-clients-ts/file-ingestion` | `+ '/nifile'` |
|
|
68
|
+
| Test Monitor | `@ni/systemlink-clients-ts/test-monitor` | `+ '/nitest'` |
|
|
69
|
+
| Asset Management | `@ni/systemlink-clients-ts/asset-management` | `+ '/niapm'` |
|
|
70
|
+
| Work Items | `@ni/systemlink-clients-ts/work-item` | `+ '/niworkorder'` |
|
|
71
|
+
| Systems Management | `@ni/systemlink-clients-ts/systems-management` | `+ '/nisysmgmt'` |
|
|
72
|
+
| Notebooks | `@ni/systemlink-clients-ts/notebook` | `+ '/ninotebook'` |
|
|
73
|
+
|
|
74
|
+
The client factory for each service lives at `@ni/systemlink-clients-ts/<service>/client`.
|
|
75
|
+
|
|
76
|
+
> **Base URL gotcha:** Each generated client's path depends on its OpenAPI spec base. For **Feeds**, the spec base is `https://host/` and operation paths already include `/nifeed/v1/...`, so use `baseUrl: window.location.origin` with no suffix. For all other services the spec root matches the service prefix (`https://host/nitag`, `https://host/niuser/v1`, etc.), so set `baseUrl: window.location.origin + '/<prefix>'` as shown above.
|
|
77
|
+
|
|
78
|
+
> **SDK type mismatch fallback:** If a generated SDK function causes `InputFieldValidationError`, verify the actual request body the server expects with a raw `curl` POST. Sometimes the generated types wrap the body in a `{ request: { ... } }` envelope that the server does not accept (or expect a flat body the type shows as nested). Use direct `fetch` with a manually constructed body as a reliable fallback when the SDK types are wrong.
|
|
79
|
+
|
|
80
|
+
### Fallback: generate a custom SDK
|
|
81
|
+
|
|
82
|
+
Only generate a new SDK if the required service is **not** in `@ni/systemlink-clients-ts`. Use [hey-api/openapi-ts](https://github.com/hey-api/openapi-ts):
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm install -D @hey-api/openapi-ts
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
// openapi-ts.config.ts
|
|
90
|
+
import { defineConfig } from '@hey-api/openapi-ts';
|
|
91
|
+
|
|
92
|
+
export default defineConfig({
|
|
93
|
+
input: 'https://<server>/swagger/v2/<service>.yaml',
|
|
94
|
+
output: { path: 'src/app/api', format: 'prettier' },
|
|
95
|
+
plugins: ['@hey-api/typescript', { name: '@hey-api/sdk' }],
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx openapi-ts
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Step 4: Wire up AppModule
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
// src/app/app.module.ts
|
|
109
|
+
import { NgModule } from '@angular/core';
|
|
110
|
+
import { BrowserModule } from '@angular/platform-browser';
|
|
111
|
+
import { FormsModule } from '@angular/forms';
|
|
112
|
+
import { APP_BASE_HREF } from '@angular/common';
|
|
113
|
+
|
|
114
|
+
// Most Nimble component modules are exported from the main `@ni/nimble-angular` barrel.
|
|
115
|
+
// Icon modules (e.g. NimbleIconMagnifyingGlassModule) are ONLY in the main barrel —
|
|
116
|
+
// sub-paths like `@ni/nimble-angular/icons/magnifying-glass` do NOT exist.
|
|
117
|
+
import {
|
|
118
|
+
NimbleThemeProviderModule,
|
|
119
|
+
NimbleButtonModule,
|
|
120
|
+
NimbleAnchorButtonModule,
|
|
121
|
+
NimbleAnchorTabsModule,
|
|
122
|
+
NimbleAnchorTabModule,
|
|
123
|
+
NimbleTextFieldModule,
|
|
124
|
+
NimbleSelectModule,
|
|
125
|
+
NimbleListOptionModule,
|
|
126
|
+
NimbleDrawerModule,
|
|
127
|
+
NimbleDialogModule,
|
|
128
|
+
NimbleSpinnerModule,
|
|
129
|
+
NimbleBannerModule,
|
|
130
|
+
NimbleTableModule,
|
|
131
|
+
NimbleTableColumnTextModule,
|
|
132
|
+
NimbleIconMagnifyingGlassModule, // icons always from main barrel
|
|
133
|
+
} from '@ni/nimble-angular';
|
|
134
|
+
// Label providers and Card have dedicated sub-path exports
|
|
135
|
+
import { NimbleLabelProviderCoreModule } from '@ni/nimble-angular/label-provider/core';
|
|
136
|
+
import { NimbleCardModule } from '@ni/nimble-angular/card';
|
|
137
|
+
|
|
138
|
+
import { AppRoutingModule } from './app-routing.module';
|
|
139
|
+
import { AppComponent } from './app.component';
|
|
140
|
+
import { MyFeatureComponent } from './my-feature/my-feature.component';
|
|
141
|
+
|
|
142
|
+
@NgModule({
|
|
143
|
+
declarations: [AppComponent, MyFeatureComponent],
|
|
144
|
+
imports: [
|
|
145
|
+
BrowserModule,
|
|
146
|
+
FormsModule,
|
|
147
|
+
AppRoutingModule,
|
|
148
|
+
NimbleThemeProviderModule,
|
|
149
|
+
NimbleLabelProviderCoreModule,
|
|
150
|
+
NimbleTableModule,
|
|
151
|
+
NimbleTableColumnTextModule,
|
|
152
|
+
NimbleButtonModule,
|
|
153
|
+
NimbleTextFieldModule,
|
|
154
|
+
NimbleSelectModule,
|
|
155
|
+
NimbleListOptionModule,
|
|
156
|
+
NimbleDrawerModule,
|
|
157
|
+
NimbleSpinnerModule,
|
|
158
|
+
NimbleBannerModule,
|
|
159
|
+
],
|
|
160
|
+
providers: [
|
|
161
|
+
{ provide: APP_BASE_HREF, useValue: '/' }, // ← REQUIRED — do not use a <base> tag
|
|
162
|
+
],
|
|
163
|
+
// Note: do NOT add provideHttpClient() — @ni/systemlink-clients-ts uses the native fetch API,
|
|
164
|
+
// not Angular's HttpClient. No HTTP DI wiring is needed.
|
|
165
|
+
bootstrap: [AppComponent],
|
|
166
|
+
})
|
|
167
|
+
export class AppModule {}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
For Nimble form controls (`nimble-text-field`, `nimble-select`, etc.), bind with Angular forms APIs (`[(ngModel)]`, `[formControl]`, or `formControlName`) and use `(ngModelChange)` for value-change reactions. Avoid native control bindings like `[value]`, `(input)`, or `(change)` on Nimble elements.
|
|
171
|
+
|
|
172
|
+
**Critical:** Provide `APP_BASE_HREF` via DI and **remove the `<base href="/">` tag from `index.html`**. SystemLink enforces a `base-uri 'self'` CSP directive; the `<base>` element violates it.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Step 5: Configure routing for SystemLink sub-path hosting
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
// src/app/app-routing.module.ts
|
|
180
|
+
import { NgModule } from '@angular/core';
|
|
181
|
+
import { RouterModule, Routes } from '@angular/router';
|
|
182
|
+
import { MyFeatureComponent } from './my-feature/my-feature.component';
|
|
183
|
+
|
|
184
|
+
const routes: Routes = [{ path: '', component: MyFeatureComponent }];
|
|
185
|
+
|
|
186
|
+
@NgModule({
|
|
187
|
+
imports: [RouterModule.forRoot(routes, { useHash: true })], // ← REQUIRED
|
|
188
|
+
exports: [RouterModule],
|
|
189
|
+
})
|
|
190
|
+
export class AppRoutingModule {}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Why `useHash: true`?** SystemLink serves your app at a sub-path like `/ni/webapps/<id>/`. Angular's default `PathLocationStrategy` tries to match the path against the route table and fails with NG04002. Hash routing (`/#/`) sidesteps this entirely.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Step 6: Fix the CSP inline-script issue
|
|
198
|
+
|
|
199
|
+
In `angular.json`, disable critical CSS inlining (the Beasties optimizer injects `onload` handlers that violate CSP `script-src 'unsafe-inline'`):
|
|
200
|
+
|
|
201
|
+
```json
|
|
202
|
+
"configurations": {
|
|
203
|
+
"production": {
|
|
204
|
+
"optimization": {
|
|
205
|
+
"scripts": true,
|
|
206
|
+
"styles": {
|
|
207
|
+
"minify": true,
|
|
208
|
+
"inlineCritical": false
|
|
209
|
+
},
|
|
210
|
+
"fonts": true
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Step 7: Configure fonts and styling with Nimble tokens
|
|
219
|
+
|
|
220
|
+
Nimble fonts (Source Sans Pro, Noto Serif) must be explicitly imported in your global styles. Split your styling tokens into two groups:
|
|
221
|
+
|
|
222
|
+
1. Theme-independent aliases such as fonts and spacing belong in `src/styles.scss` on `:root`
|
|
223
|
+
2. Theme-aware aliases such as colors and shadows must be defined on `nimble-theme-provider`, because Nimble resolves its theme tokens there rather than on `:root`
|
|
224
|
+
|
|
225
|
+
### Import Nimble fonts (required)
|
|
226
|
+
|
|
227
|
+
```scss
|
|
228
|
+
/* src/styles.scss */
|
|
229
|
+
|
|
230
|
+
/* Import Nimble fonts (Source Sans Pro, Noto Serif) */
|
|
231
|
+
@use '@ni/nimble-angular/styles/fonts' as *;
|
|
232
|
+
|
|
233
|
+
/* Theme-independent aliases only. */
|
|
234
|
+
:root {
|
|
235
|
+
/* Typography - map to Nimble's named font tokens */
|
|
236
|
+
--sl-app-font-body: var(--ni-nimble-body-font);
|
|
237
|
+
--sl-app-font-body-emphasized: var(--ni-nimble-body-emphasized-font);
|
|
238
|
+
--sl-app-font-title: var(--ni-nimble-title-font);
|
|
239
|
+
--sl-app-font-title-plus-1: var(--ni-nimble-title-plus-1-font);
|
|
240
|
+
--sl-app-font-control-label: var(--ni-nimble-control-label-font);
|
|
241
|
+
--sl-app-font-group-header: var(--ni-nimble-group-header-font);
|
|
242
|
+
|
|
243
|
+
--sl-app-space-1: var(--ni-nimble-small-padding, 4px);
|
|
244
|
+
--sl-app-space-2: var(--ni-nimble-medium-padding, 8px);
|
|
245
|
+
--sl-app-space-3: calc(var(--ni-nimble-small-padding, 4px) * 3);
|
|
246
|
+
--sl-app-space-4: var(--ni-nimble-standard-padding, 16px);
|
|
247
|
+
--sl-app-space-6: var(--ni-nimble-large-padding, 24px);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* Apply body defaults */
|
|
251
|
+
html,
|
|
252
|
+
body {
|
|
253
|
+
margin: 0;
|
|
254
|
+
min-height: 100%;
|
|
255
|
+
font: var(--sl-app-font-body);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
h1 { font: var(--sl-app-font-title-plus-1); }
|
|
259
|
+
h2 { font: var(--sl-app-font-title); }
|
|
260
|
+
h3, h4, h5, h6 { font: var(--sl-app-font-body-emphasized); }
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Define theme-aware aliases on the root `nimble-theme-provider` instead of `:root`:
|
|
264
|
+
|
|
265
|
+
```scss
|
|
266
|
+
/* src/app/app.component.scss */
|
|
267
|
+
|
|
268
|
+
:host {
|
|
269
|
+
display: block;
|
|
270
|
+
height: 100vh;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
nimble-theme-provider {
|
|
274
|
+
display: block;
|
|
275
|
+
height: 100%;
|
|
276
|
+
background: var(--ni-nimble-application-background-color);
|
|
277
|
+
color: var(--ni-nimble-body-font-color);
|
|
278
|
+
|
|
279
|
+
--sl-app-color-bg: var(--ni-nimble-application-background-color);
|
|
280
|
+
--sl-app-color-surface: var(--ni-nimble-section-background-color);
|
|
281
|
+
--sl-app-color-surface-alt: var(--ni-nimble-header-background-color);
|
|
282
|
+
--sl-app-color-border: var(--ni-nimble-divider-background-color); /* use for dividers and section separators */
|
|
283
|
+
--sl-app-color-border-strong: var(--ni-nimble-popup-border-color);
|
|
284
|
+
--sl-app-color-text: var(--ni-nimble-body-font-color);
|
|
285
|
+
--sl-app-color-text-muted: var(--ni-nimble-placeholder-font-color);
|
|
286
|
+
--sl-app-color-accent: var(--ni-nimble-button-fill-primary-color);
|
|
287
|
+
--sl-app-color-accent-contrast: var(--ni-nimble-button-primary-font-color);
|
|
288
|
+
--sl-app-color-success: var(--ni-nimble-pass-color);
|
|
289
|
+
--sl-app-shadow-1: var(--ni-nimble-elevation-1-box-shadow);
|
|
290
|
+
--sl-app-shadow-2: var(--ni-nimble-elevation-2-box-shadow);
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Do not add literal color fallbacks to theme-aware aliases. If you write `var(--ni-nimble-application-background-color, #fff)` into your app token layer, you make it too easy to miss a broken theme hookup and accidentally freeze the palette to a light-only fallback.
|
|
295
|
+
|
|
296
|
+
### Use semantic tokens in component SCSS
|
|
297
|
+
|
|
298
|
+
Instead of hard-coded colors/sizes, reference the semantic `--sl-app-*` variables:
|
|
299
|
+
|
|
300
|
+
```scss
|
|
301
|
+
// src/app/my-feature/my-feature.component.scss
|
|
302
|
+
|
|
303
|
+
// Clickable card — use Nimble card-specific tokens directly (more specific than --sl-app-color-border)
|
|
304
|
+
.card {
|
|
305
|
+
padding: var(--sl-app-space-4);
|
|
306
|
+
border: 1px solid var(--ni-nimble-card-border-color);
|
|
307
|
+
background: var(--ni-nimble-section-background-color);
|
|
308
|
+
border-radius: var(--sl-app-space-1);
|
|
309
|
+
cursor: pointer;
|
|
310
|
+
transition: box-shadow var(--ni-nimble-medium-delay, 0.15s) ease,
|
|
311
|
+
border-color var(--ni-nimble-medium-delay, 0.15s) ease;
|
|
312
|
+
|
|
313
|
+
&:hover {
|
|
314
|
+
box-shadow: var(--ni-nimble-elevation-2-box-shadow, 0 2px 8px rgba(0, 0, 0, 0.12));
|
|
315
|
+
border-color: var(--ni-nimble-border-hover-color);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.card-title {
|
|
320
|
+
font: var(--sl-app-font-body-emphasized);
|
|
321
|
+
color: var(--sl-app-color-text);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.card-meta {
|
|
325
|
+
font: var(--sl-app-font-control-label);
|
|
326
|
+
color: var(--sl-app-color-text-muted);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
If you want compile-time token values in SCSS, you can also import Nimble's token variables:
|
|
331
|
+
|
|
332
|
+
```scss
|
|
333
|
+
@use '@ni/nimble-angular/styles/tokens' as *;
|
|
334
|
+
|
|
335
|
+
.my-element {
|
|
336
|
+
color: $ni-nimble-body-font-color;
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Why this pattern?
|
|
341
|
+
|
|
342
|
+
1. **Themability** — All colors flow through Nimble's theme-aware tokens. If Nimble changes color ramps or adds dark mode, your app automatically inherits it.
|
|
343
|
+
2. **Consistency** — Using Nimble's canonical fonts (Source Sans Pro for UI, Noto Serif for headings) ensures your app feels native to SystemLink.
|
|
344
|
+
3. **Typography scales** — Nimble defines font sizes, weights, and line heights per role (body, titles, labels, headings). Reuse them rather than inventing your own.
|
|
345
|
+
4. **Correct token resolution** — Color and shadow aliases must live on `nimble-theme-provider`; defining them on `:root` can leave a hosted app stuck on the wrong palette even when the provider's `theme` attribute changes.
|
|
346
|
+
5. **Responsive spacing** — Nimble's padding tokens scale predictably; build layouts that adapt to different screen sizes by composing space variables.
|
|
347
|
+
|
|
348
|
+
### Available Nimble tokens
|
|
349
|
+
|
|
350
|
+
See [Nimble's theme-aware tokens documentation](https://nimble.ni.dev/storybook/index.html?path=/docs/tokens-theme-aware-tokens--docs) for a complete token reference (colors, dimensions, shadows, delays, etc.).
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Step 8: Call SystemLink APIs
|
|
355
|
+
|
|
356
|
+
### Configure the client at runtime
|
|
357
|
+
|
|
358
|
+
Every `@ni/systemlink-clients-ts` service exposes `createClient` and `createConfig` from its `/client` subpath. Always create a configured client at call-site (or lazily inside a helper) using values from `window.location.origin` and optionally `localStorage` — never rely on the package's default `baseUrl`.
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import { createClient, createConfig } from '@ni/systemlink-clients-ts/file-ingestion/client';
|
|
362
|
+
import { queryFilesLinq } from '@ni/systemlink-clients-ts/file-ingestion';
|
|
363
|
+
|
|
364
|
+
function buildClient() {
|
|
365
|
+
const baseUrl = localStorage.getItem('sl_api_url') ?? `${window.location.origin}/nifile`;
|
|
366
|
+
const apiKey = localStorage.getItem('sl_api_key');
|
|
367
|
+
return createClient(createConfig({
|
|
368
|
+
baseUrl,
|
|
369
|
+
headers: apiKey ? { 'x-ni-api-key': apiKey } : {},
|
|
370
|
+
credentials: apiKey ? 'omit' : 'include', // cookie auth when no API key
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Use in a component method:
|
|
375
|
+
const { data, error } = await queryFilesLinq({ client: buildClient(), body: { take: 100 } });
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
For **ad-hoc POST calls** to endpoints not yet covered by an SDK function, use the client directly:
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
const { data, error } = await buildClient().post<MyResponse, unknown>({
|
|
382
|
+
url: '/v1/service-groups/Default/search-files',
|
|
383
|
+
body: { filter: 'name:("*report*")', take: 100 },
|
|
384
|
+
headers: { 'Content-Type': 'application/json' },
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Base URL reference
|
|
389
|
+
|
|
390
|
+
Always compute the base URL from `window.location.origin` — never hardcode a hostname:
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
const BASE_URL = `${window.location.origin}/nitag`; // Tags
|
|
394
|
+
const BASE_URL = `${window.location.origin}/nitest`; // Test Monitor
|
|
395
|
+
const BASE_URL = `${window.location.origin}/niapm`; // Asset Management
|
|
396
|
+
const BASE_URL = `${window.location.origin}/nifile`; // File Ingestion
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Authentication
|
|
400
|
+
|
|
401
|
+
- **Same-origin** (app and API on the same server): use `credentials: 'include'` — session cookies are sent automatically, no API key needed.
|
|
402
|
+
- **Remote / dev**: read an API key from `localStorage` and pass it as `x-ni-api-key` header. Set `credentials: 'omit'` when using an API key.
|
|
403
|
+
- Never hardcode credentials in source code.
|
|
404
|
+
|
|
405
|
+
### Querying
|
|
406
|
+
|
|
407
|
+
- Build queries as typed objects matching the SDK models — don't construct raw URL strings
|
|
408
|
+
- For LINQ filter strings (tags, files), keep filters simple: `path = "..."`, `type = "..."`, `name:("*pattern*")`
|
|
409
|
+
- Avoid `projection` parameters unless you fully understand how they reshape the response — they often flatten nested objects and break your mapping logic
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Step 9: App template pattern
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
// src/app/app.component.ts
|
|
417
|
+
import { Component, OnDestroy, OnInit } from '@angular/core';
|
|
418
|
+
|
|
419
|
+
@Component({
|
|
420
|
+
selector: 'app-root',
|
|
421
|
+
template: `
|
|
422
|
+
<nimble-theme-provider [theme]="currentTheme">
|
|
423
|
+
<nimble-label-provider-core withDefaults></nimble-label-provider-core>
|
|
424
|
+
<router-outlet></router-outlet>
|
|
425
|
+
</nimble-theme-provider>
|
|
426
|
+
`,
|
|
427
|
+
})
|
|
428
|
+
export class AppComponent implements OnInit, OnDestroy {
|
|
429
|
+
currentTheme: 'light' | 'dark' = 'light';
|
|
430
|
+
private themeObserver: MutationObserver | null = null;
|
|
431
|
+
|
|
432
|
+
ngOnInit(): void {
|
|
433
|
+
this.currentTheme = this.detectInitialTheme();
|
|
434
|
+
this.watchParentTheme();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
ngOnDestroy(): void {
|
|
438
|
+
this.themeObserver?.disconnect();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private detectInitialTheme(): 'light' | 'dark' {
|
|
442
|
+
try {
|
|
443
|
+
const params = new URLSearchParams(window.location.search);
|
|
444
|
+
const queryTheme = params.get('theme');
|
|
445
|
+
if (queryTheme === 'light' || queryTheme === 'dark') return queryTheme;
|
|
446
|
+
} catch {}
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
if (window.parent !== window) {
|
|
450
|
+
const parentProvider = window.parent.document.querySelector('nimble-theme-provider');
|
|
451
|
+
const parentTheme = parentProvider?.getAttribute('theme');
|
|
452
|
+
if (parentTheme === 'light' || parentTheme === 'dark') return parentTheme;
|
|
453
|
+
}
|
|
454
|
+
} catch {}
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
const savedTheme = localStorage.getItem('sl_app_theme');
|
|
458
|
+
if (savedTheme === 'light' || savedTheme === 'dark') return savedTheme;
|
|
459
|
+
} catch {}
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) return 'dark';
|
|
463
|
+
} catch {}
|
|
464
|
+
|
|
465
|
+
return 'light';
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private watchParentTheme(): void {
|
|
469
|
+
try {
|
|
470
|
+
if (window.parent === window) return;
|
|
471
|
+
const parentProvider = window.parent.document.querySelector('nimble-theme-provider');
|
|
472
|
+
if (!parentProvider) return;
|
|
473
|
+
|
|
474
|
+
this.themeObserver = new MutationObserver(() => {
|
|
475
|
+
const parentTheme = parentProvider.getAttribute('theme');
|
|
476
|
+
if (parentTheme === 'light' || parentTheme === 'dark') {
|
|
477
|
+
this.currentTheme = parentTheme;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
this.themeObserver.observe(parentProvider, {
|
|
482
|
+
attributes: true,
|
|
483
|
+
attributeFilter: ['theme'],
|
|
484
|
+
});
|
|
485
|
+
} catch {}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### How theme sync works
|
|
491
|
+
|
|
492
|
+
SystemLink's Web Apps shell renders each webapp inside a **same-origin `<iframe>`**. Because host and child share the same origin, the iframe's JavaScript can read and observe the parent document's DOM.
|
|
493
|
+
|
|
494
|
+
#### Initial detection — priority cascade
|
|
495
|
+
|
|
496
|
+
`detectInitialTheme()` resolves the starting theme by checking sources in order:
|
|
497
|
+
|
|
498
|
+
| Priority | Source | Why |
|
|
499
|
+
|----------|--------|-----|
|
|
500
|
+
| 1 | `?theme=dark` URL query parameter | Easy override for dev/testing without needing the full shell |
|
|
501
|
+
| 2 | Parent frame's `nimble-theme-provider[theme]` attribute | SystemLink's shell owns a `<nimble-theme-provider>` element; reading its `theme` attribute gives the exact theme the shell is currently displaying |
|
|
502
|
+
| 3 | `localStorage.getItem('sl_app_theme')` | Remembers a previously chosen preference when running standalone (outside the shell) |
|
|
503
|
+
| 4 | `window.matchMedia('(prefers-color-scheme: dark)')` | OS-level dark mode when no other signal is available |
|
|
504
|
+
| 5 | `'light'` | Safe default |
|
|
505
|
+
|
|
506
|
+
Each priority block is wrapped in its own `try/catch` so a failure in one (e.g. cross-origin access, missing API) does not prevent the lower priorities from being evaluated.
|
|
507
|
+
|
|
508
|
+
#### Dynamic updates — MutationObserver on the parent provider
|
|
509
|
+
|
|
510
|
+
`watchParentTheme()` installs a `MutationObserver` on the parent document's `nimble-theme-provider` element:
|
|
511
|
+
|
|
512
|
+
```typescript
|
|
513
|
+
this.themeObserver = new MutationObserver(() => {
|
|
514
|
+
const t = parentProvider.getAttribute('theme');
|
|
515
|
+
if (t === 'light' || t === 'dark') this.currentTheme = t;
|
|
516
|
+
});
|
|
517
|
+
this.themeObserver.observe(parentProvider, {
|
|
518
|
+
attributes: true,
|
|
519
|
+
attributeFilter: ['theme'], // only fires when the `theme` attribute mutates
|
|
520
|
+
});
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
When the SystemLink user toggles the theme in the shell, the shell updates its `nimble-theme-provider theme="dark|light"` attribute. The `MutationObserver` callback fires immediately (synchronously in the microtask queue), Angular's change detection picks up the new `currentTheme` value, and `<nimble-theme-provider [theme]="currentTheme">` re-renders with the correct token set. The transition happens with no perceptible lag.
|
|
524
|
+
|
|
525
|
+
#### Template binding
|
|
526
|
+
|
|
527
|
+
The root component template must bind `currentTheme` directly to the `<nimble-theme-provider>` element that wraps all app content:
|
|
528
|
+
|
|
529
|
+
```html
|
|
530
|
+
<nimble-theme-provider [theme]="currentTheme">
|
|
531
|
+
<nimble-label-provider-core withDefaults></nimble-label-provider-core>
|
|
532
|
+
<!-- all app content here -->
|
|
533
|
+
<router-outlet></router-outlet>
|
|
534
|
+
</nimble-theme-provider>
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Nimble's theme provider resolves its design tokens (colors, shadows, etc.) based on its own `theme` property. Every `--ni-nimble-*` CSS variable inside the provider's subtree updates when the property changes.
|
|
538
|
+
|
|
539
|
+
#### Cleanup in `ngOnDestroy`
|
|
540
|
+
|
|
541
|
+
The observer holds a reference to the parent DOM element. Always disconnect it when the component is destroyed to prevent memory leaks:
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
ngOnDestroy(): void {
|
|
545
|
+
this.themeObserver?.disconnect();
|
|
546
|
+
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
#### Cross-origin and standalone safety
|
|
550
|
+
|
|
551
|
+
All `window.parent.document` access is wrapped in `try/catch`. If the app is:
|
|
552
|
+
- opened directly in a browser tab (`window.parent === window`) → the guard `if (window.parent === window) return;` exits early
|
|
553
|
+
- embedded in a cross-origin frame → accessing `window.parent.document` throws a `SecurityError`; the `catch {}` silently swallows it and the app falls through to localStorage / system preference
|
|
554
|
+
- embedded same-origin (production SystemLink) → fully works
|
|
555
|
+
|
|
556
|
+
This pattern requires zero configuration — the same binary works correctly in all three contexts.
|
|
557
|
+
|
|
558
|
+
### nimble-anchor-tabs navigation
|
|
559
|
+
|
|
560
|
+
For top-level navigation, use `<nimble-anchor-tabs>` with `[activeid]` and `nimbleRouterLink` on each tab. Track the active tab by subscribing to Angular's `NavigationEnd` events:
|
|
561
|
+
|
|
562
|
+
```html
|
|
563
|
+
<nimble-anchor-tabs [activeid]="activeTabId">
|
|
564
|
+
<nimble-anchor-tab id="catalog" nimbleRouterLink="/catalog">Catalog</nimble-anchor-tab>
|
|
565
|
+
<nimble-anchor-tab id="installed" nimbleRouterLink="/installed">Installed</nimble-anchor-tab>
|
|
566
|
+
<nimble-anchor-tab id="settings" nimbleRouterLink="/settings">Settings</nimble-anchor-tab>
|
|
567
|
+
</nimble-anchor-tabs>
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
```typescript
|
|
571
|
+
import { Router, NavigationEnd } from '@angular/router';
|
|
572
|
+
import { Subscription } from 'rxjs';
|
|
573
|
+
import { filter } from 'rxjs/operators';
|
|
574
|
+
|
|
575
|
+
export class AppComponent implements OnInit, OnDestroy {
|
|
576
|
+
activeTabId = 'catalog';
|
|
577
|
+
private routerSub?: Subscription;
|
|
578
|
+
|
|
579
|
+
constructor(private router: Router) {}
|
|
580
|
+
|
|
581
|
+
ngOnInit(): void {
|
|
582
|
+
this.activeTabId = this.tabIdFromUrl(this.router.url);
|
|
583
|
+
this.routerSub = this.router.events.pipe(
|
|
584
|
+
filter(e => e instanceof NavigationEnd)
|
|
585
|
+
).subscribe(e => {
|
|
586
|
+
this.activeTabId = this.tabIdFromUrl((e as NavigationEnd).urlAfterRedirects);
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
ngOnDestroy(): void { this.routerSub?.unsubscribe(); }
|
|
591
|
+
|
|
592
|
+
private tabIdFromUrl(url: string): string {
|
|
593
|
+
if (url.startsWith('/installed')) return 'installed';
|
|
594
|
+
if (url.startsWith('/settings')) return 'settings';
|
|
595
|
+
return 'catalog';
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
Required modules: `NimbleAnchorTabsModule`, `NimbleAnchorTabModule` — both from `@ni/nimble-angular`.
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
604
|
+
## Step 10: Build
|
|
605
|
+
|
|
606
|
+
```bash
|
|
607
|
+
node_modules/.bin/ng build --configuration production --output-path dist/<app-name>
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
- Do **not** pass `--base-href` — that would re-introduce the `<base>` element
|
|
611
|
+
- Output goes to `dist/<app-name>/browser/` (Angular 19)
|
|
612
|
+
|
|
613
|
+
If you hit budget errors, increase limits in `angular.json`:
|
|
614
|
+
|
|
615
|
+
```json
|
|
616
|
+
"budgets": [
|
|
617
|
+
{ "type": "initial", "maximumWarning": "1MB", "maximumError": "2MB" },
|
|
618
|
+
{ "type": "anyComponentStyle", "maximumWarning": "2KB", "maximumError": "4KB" }
|
|
619
|
+
]
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## Step 11: Deploy with slcli
|
|
625
|
+
|
|
626
|
+
```bash
|
|
627
|
+
# First deploy — no existing webapp ID
|
|
628
|
+
slcli webapp publish dist/<app-name>/browser/ -w <workspace-name>
|
|
629
|
+
|
|
630
|
+
# Update existing webapp
|
|
631
|
+
slcli webapp publish dist/<app-name>/browser/ -w <workspace-name> -i <webapp-id>
|
|
632
|
+
|
|
633
|
+
# Open in browser
|
|
634
|
+
slcli webapp open -i <webapp-id>
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Save the returned webapp ID — you'll need it for every subsequent redeploy.
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Troubleshooting quick-reference
|
|
642
|
+
|
|
643
|
+
| Symptom | Cause | Fix |
|
|
644
|
+
|---------|-------|-----|
|
|
645
|
+
| CSP `base-uri` error | `<base href="/">` in index.html | Remove `<base>` tag; provide `APP_BASE_HREF` via DI |
|
|
646
|
+
| NG04002 / white screen | PathLocationStrategy can't resolve sub-path | `useHash: true` in RouterModule |
|
|
647
|
+
| CSP `unsafe-inline` error | Beasties injects `onload` in style tags | `inlineCritical: false` in angular.json optimization |
|
|
648
|
+
| App stays light inside dark SystemLink shell | Theme-aware aliases defined on `:root` or embedded app not watching host provider | Define color/shadow aliases on `nimble-theme-provider`; sync `currentTheme` from parent provider |
|
|
649
|
+
| `theme="dark"` is set but colors still look light | Checked the attribute only, not the resolved tokens | Inspect `getComputedStyle(themeProvider).getPropertyValue('--ni-nimble-application-background-color')` in the hosted iframe |
|
|
650
|
+
| CORS / status 0 | `basePath` points to different origin | Set `basePath = window.location.origin + '/service-prefix'` |
|
|
651
|
+
| 404 on API calls | Missing service prefix in base URL | e.g., `/nitag` not just `window.location.origin`; Feeds service paths already include `/nifeed/v1/` so use origin alone |
|
|
652
|
+
| `InputFieldValidationError` on API call | SDK-generated request body has wrong shape | Inspect raw API; the generated type may add or omit a `request: {}` wrapper. Use direct `fetch` with manually constructed body |
|
|
653
|
+
| nimble-dialog does not open | `*ngIf` destroys element before `ViewChild` can resolve | Remove `*ngIf` from the dialog element; use `@ViewChild` + `ElementRef` and call `nativeElement.show()` / `nativeElement.close()` |
|
|
654
|
+
| Icon module import fails | Icon sub-path `@ni/nimble-angular/icons/...` does not exist | Import icon modules from the main `@ni/nimble-angular` barrel only |
|
|
655
|
+
| Table rows empty despite correct response | `projection` flattens nested objects | Remove `projection` from query body |
|
|
656
|
+
| `TableRecord` type error | Row type missing index signature | Add `[key: string]: FieldValue \| undefined` |
|
|
657
|
+
| Button appearance invalid | Wrong value for `appearance` attr | Use `appearance="block" appearance-variant="accent"` |
|
|
658
|
+
| `ng build` exits 130 / truncated | Terminal heredoc issue in VS Code | Run build as background process: `nohup ng build ... > /tmp/build.log 2>&1 &` |
|
|
659
|
+
|
|
660
|
+
### Hosted theme validation recipe
|
|
661
|
+
|
|
662
|
+
When validating a deployed SystemLink webapp, prefer checking the hosted instance rather than only local dev:
|
|
663
|
+
|
|
664
|
+
1. Open the webapp inside SystemLink so you can inspect the shell and embedded iframe together
|
|
665
|
+
2. Verify both parent and iframe expose a `nimble-theme-provider`
|
|
666
|
+
3. Compare resolved token values, not just attributes, for example `--ni-nimble-application-background-color`, `--ni-nimble-header-background-color`, and `--ni-nimble-body-font-color`
|
|
667
|
+
4. Scan component SCSS for hard-coded color literals and replace them with Nimble tokens or local semantic aliases
|
|
668
|
+
|
|
669
|
+
If the host and iframe are same-origin, Playwright or DevTools can inspect `iframe.contentDocument` directly.
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## Known SystemLink service prefixes
|
|
674
|
+
|
|
675
|
+
| Service | URL prefix |
|
|
676
|
+
|---------|-----------|
|
|
677
|
+
| Tag Historian | `/nitag/v2` |
|
|
678
|
+
| Test Monitor | `/nitest` |
|
|
679
|
+
| Asset Management | `/niapm` |
|
|
680
|
+
| Systems Management | `/nisysmgmt` |
|
|
681
|
+
| Work Orders | `/niworkorder` |
|
|
682
|
+
| Feeds (Package Manager) | `/nifeed` |
|
|
683
|
+
| Files | `/nifile` |
|
|
684
|
+
| Notebooks | `/ninotebook` |
|
|
685
|
+
|
|
686
|
+
See `references/systemlink-services.md` for full API details.
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## nimble-dialog — imperative pattern
|
|
691
|
+
|
|
692
|
+
Do NOT use `*ngIf` on a `nimble-dialog`. When `*ngIf` is false the element is removed from the DOM, so `@ViewChild` cannot resolve it and `.show()` will never be called.
|
|
693
|
+
|
|
694
|
+
```html
|
|
695
|
+
<!-- Always keep the dialog in the DOM; never *ngIf it -->
|
|
696
|
+
<nimble-dialog #myDialog>
|
|
697
|
+
<span slot="title">Dialog Title</span>
|
|
698
|
+
<span slot="subtitle">Optional subtitle or instruction</span>
|
|
699
|
+
|
|
700
|
+
<!-- dialog body content -->
|
|
701
|
+
<nimble-select #mySelect filter-mode="standard" [(ngModel)]="selectedValue">
|
|
702
|
+
<nimble-list-option *ngFor="let opt of options" [value]="opt.id">{{ opt.name }}</nimble-list-option>
|
|
703
|
+
</nimble-select>
|
|
704
|
+
|
|
705
|
+
<nimble-button slot="footer" (click)="closeDialog()">Cancel</nimble-button>
|
|
706
|
+
<nimble-button slot="footer" (click)="applyDialog()" [disabled]="applying">Apply</nimble-button>
|
|
707
|
+
</nimble-dialog>
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
import { ElementRef, ViewChild } from '@angular/core';
|
|
712
|
+
|
|
713
|
+
@ViewChild('myDialog') private dialogEl?: ElementRef;
|
|
714
|
+
@ViewChild('mySelect') private selectEl?: ElementRef;
|
|
715
|
+
|
|
716
|
+
openDialog(): void {
|
|
717
|
+
this.dialogEl?.nativeElement.show();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
closeDialog(): void {
|
|
721
|
+
this.dialogEl?.nativeElement.close();
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
**Slot summary:** `slot="title"` (required), `slot="subtitle"` (optional), `slot="footer"` (buttons — can have multiple).
|
|
726
|
+
|
|
727
|
+
Required module: `NimbleDialogModule` from `@ni/nimble-angular`.
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
|
|
731
|
+
## Key imports reference
|
|
732
|
+
|
|
733
|
+
| Item | Import path |
|
|
734
|
+
|------|-------------|
|
|
735
|
+
| Most component modules (theme provider, buttons, anchor-buttons, anchor-tabs, anchor-tab, tabs, tab, tab-panel, dialog, drawer, inputs, select, list-option, spinner, banner, toolbar, menu, table) | `@ni/nimble-angular` |
|
|
736
|
+
| **Icon modules** (e.g. `NimbleIconMagnifyingGlassModule`) | `@ni/nimble-angular` — **must use main barrel; icon sub-paths do not exist** |
|
|
737
|
+
| Label provider core module | `@ni/nimble-angular/label-provider/core` |
|
|
738
|
+
| Label provider rich text module | `@ni/nimble-angular/label-provider/rich-text` |
|
|
739
|
+
| Label provider table module | `@ni/nimble-angular/label-provider/table` |
|
|
740
|
+
| Card module | `@ni/nimble-angular/card` |
|
|
741
|
+
| Fonts styles entrypoint (`@use`) | `@ni/nimble-angular/styles/fonts` |
|
|
742
|
+
| Tokens styles entrypoint (`@use`) | `@ni/nimble-angular/styles/tokens` |
|
|
743
|
+
|
|
744
|
+
See `references/nimble-angular.md` for template usage of each component.
|