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.
Files changed (74) hide show
  1. slcli/__init__.py +1 -0
  2. slcli/__main__.py +23 -0
  3. slcli/_version.py +4 -0
  4. slcli/asset_click.py +1289 -0
  5. slcli/cli_formatters.py +218 -0
  6. slcli/cli_utils.py +504 -0
  7. slcli/comment_click.py +602 -0
  8. slcli/completion_click.py +418 -0
  9. slcli/config.py +81 -0
  10. slcli/config_click.py +498 -0
  11. slcli/dff_click.py +979 -0
  12. slcli/dff_decorators.py +24 -0
  13. slcli/example_click.py +404 -0
  14. slcli/example_loader.py +274 -0
  15. slcli/example_provisioner.py +2777 -0
  16. slcli/examples/README.md +134 -0
  17. slcli/examples/_schema/schema-v1.0.json +169 -0
  18. slcli/examples/demo-complete-workflow/README.md +323 -0
  19. slcli/examples/demo-complete-workflow/config.yaml +638 -0
  20. slcli/examples/demo-test-plans/README.md +132 -0
  21. slcli/examples/demo-test-plans/config.yaml +154 -0
  22. slcli/examples/exercise-5-1-parametric-insights/README.md +101 -0
  23. slcli/examples/exercise-5-1-parametric-insights/config.yaml +1589 -0
  24. slcli/examples/exercise-7-1-test-plans/README.md +93 -0
  25. slcli/examples/exercise-7-1-test-plans/config.yaml +323 -0
  26. slcli/examples/spec-compliance-notebooks/README.md +140 -0
  27. slcli/examples/spec-compliance-notebooks/config.yaml +112 -0
  28. slcli/examples/spec-compliance-notebooks/notebooks/SpecAnalysis_ComplianceCalculation.ipynb +1553 -0
  29. slcli/examples/spec-compliance-notebooks/notebooks/SpecComplianceCalculation.ipynb +1577 -0
  30. slcli/examples/spec-compliance-notebooks/notebooks/SpecfileExtractionAndIngestion.ipynb +912 -0
  31. slcli/examples/spec-compliance-notebooks/spec_template.xlsx +0 -0
  32. slcli/feed_click.py +892 -0
  33. slcli/file_click.py +932 -0
  34. slcli/function_click.py +1400 -0
  35. slcli/function_templates.py +85 -0
  36. slcli/main.py +406 -0
  37. slcli/mcp_click.py +269 -0
  38. slcli/mcp_server.py +748 -0
  39. slcli/notebook_click.py +1770 -0
  40. slcli/platform.py +345 -0
  41. slcli/policy_click.py +679 -0
  42. slcli/policy_utils.py +411 -0
  43. slcli/profiles.py +411 -0
  44. slcli/response_handlers.py +359 -0
  45. slcli/routine_click.py +763 -0
  46. slcli/skill_click.py +253 -0
  47. slcli/skills/slcli/SKILL.md +713 -0
  48. slcli/skills/slcli/references/analysis-recipes.md +474 -0
  49. slcli/skills/slcli/references/filtering.md +236 -0
  50. slcli/skills/systemlink-webapp/SKILL.md +744 -0
  51. slcli/skills/systemlink-webapp/references/deployment.md +123 -0
  52. slcli/skills/systemlink-webapp/references/nimble-angular.md +380 -0
  53. slcli/skills/systemlink-webapp/references/systemlink-services.md +192 -0
  54. slcli/ssl_trust.py +93 -0
  55. slcli/system_click.py +2216 -0
  56. slcli/table_utils.py +124 -0
  57. slcli/tag_click.py +794 -0
  58. slcli/templates_click.py +599 -0
  59. slcli/testmonitor_click.py +1667 -0
  60. slcli/universal_handlers.py +305 -0
  61. slcli/user_click.py +1218 -0
  62. slcli/utils.py +832 -0
  63. slcli/web_editor.py +295 -0
  64. slcli/webapp_click.py +981 -0
  65. slcli/workflow_preview.py +287 -0
  66. slcli/workflows_click.py +988 -0
  67. slcli/workitem_click.py +2258 -0
  68. slcli/workspace_click.py +576 -0
  69. slcli/workspace_utils.py +206 -0
  70. systemlink_cli-1.3.1.dist-info/METADATA +20 -0
  71. systemlink_cli-1.3.1.dist-info/RECORD +74 -0
  72. systemlink_cli-1.3.1.dist-info/WHEEL +4 -0
  73. systemlink_cli-1.3.1.dist-info/entry_points.txt +7 -0
  74. 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.