├── .env.production ├── apps ├── react-starter │ ├── .prettierrc │ ├── src │ │ ├── pages │ │ │ ├── user-settings │ │ │ │ ├── brand.png │ │ │ │ ├── classic.png │ │ │ │ ├── styles.module.css │ │ │ │ └── index.tsx │ │ │ ├── overview │ │ │ │ ├── components │ │ │ │ │ ├── device-range │ │ │ │ │ │ ├── styles.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── status-history │ │ │ │ │ │ ├── styles.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── incidents │ │ │ │ │ │ └── overview │ │ │ │ │ │ │ ├── incident.ts │ │ │ │ │ │ │ ├── styles.module.css │ │ │ │ │ │ │ ├── incident-list │ │ │ │ │ │ │ └── styles.module.css │ │ │ │ │ │ │ └── overview.test.tsx │ │ │ │ │ ├── overview │ │ │ │ │ │ ├── styles.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── quick-actions │ │ │ │ │ │ └── index.tsx │ │ │ │ ├── styles.module.css │ │ │ │ └── index.tsx │ │ │ ├── devices │ │ │ │ ├── components │ │ │ │ │ ├── modal │ │ │ │ │ │ ├── styles.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── device-details │ │ │ │ │ │ ├── styles.module.css │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── ag-grid-table │ │ │ │ │ │ ├── styles.module.css │ │ │ │ │ │ ├── device-cell-renderer.tsx │ │ │ │ │ │ └── delete-modal.tsx │ │ │ │ └── styles.module.css │ │ │ └── application-settings │ │ │ │ ├── styles.module.css │ │ │ │ └── index.tsx │ │ ├── index.css │ │ ├── setupTests.ts │ │ ├── vite-env.d.ts │ │ ├── util │ │ │ ├── mock-api.ts │ │ │ └── util.ts │ │ ├── types │ │ │ └── index.tsx │ │ ├── hooks │ │ │ ├── theme.ts │ │ │ ├── mediaQuery.ts │ │ │ └── demoMessage.ts │ │ ├── i18n.tsx │ │ ├── store │ │ │ ├── hooks │ │ │ │ └── device.ts │ │ │ └── device-store.ts │ │ ├── main.tsx │ │ ├── App.tsx │ │ ├── locales │ │ │ ├── en │ │ │ │ └── translation.json │ │ │ └── de │ │ │ │ └── translation.json │ │ └── Logo.tsx │ ├── tsconfig.json │ ├── README.md │ ├── .gitignore │ ├── index.html │ ├── tsconfig.node.json │ ├── tsconfig.app.json │ ├── eslint.config.js │ ├── vite.config.ts │ ├── public │ │ └── vite.svg │ ├── package.json │ ├── e2e │ │ └── devices.e2e.ts │ └── playwright.config.ts └── angular-starter │ ├── src │ ├── assets │ │ ├── theme │ │ │ └── theme.scss │ │ ├── images │ │ │ ├── brand.png │ │ │ └── classic.png │ │ ├── mock-data │ │ │ └── Incidents.ts │ │ └── i18n │ │ │ ├── en.json │ │ │ └── de.json │ ├── environments │ │ └── environments.ts │ ├── styles.scss │ ├── app │ │ ├── pages │ │ │ ├── devices │ │ │ │ ├── components │ │ │ │ │ ├── delete-modal │ │ │ │ │ │ ├── delete-modal.component.scss │ │ │ │ │ │ ├── delete-modal.component.html │ │ │ │ │ │ ├── delete-modal.component.ts │ │ │ │ │ │ └── delete-modal.component.spec.ts │ │ │ │ │ ├── add-device-model │ │ │ │ │ │ ├── add-device-model.component.scss │ │ │ │ │ │ ├── add-device-model.component.spec.ts │ │ │ │ │ │ ├── add-device-model.component.ts │ │ │ │ │ │ └── add-device-model.component.html │ │ │ │ │ ├── device-cell-renderer │ │ │ │ │ │ ├── device-cell-renderer.component.html │ │ │ │ │ │ ├── device-cell-renderer.component.scss │ │ │ │ │ │ ├── device-cell-renderer.component.spec.ts │ │ │ │ │ │ └── device-cell-renderer.component.ts │ │ │ │ │ └── action-cell-renderer │ │ │ │ │ │ ├── action-cell-renderer.component.scss │ │ │ │ │ │ ├── action-cell-renderer.component.spec.ts │ │ │ │ │ │ └── action-cell-renderer.component.html │ │ │ │ ├── devices.component.scss │ │ │ │ ├── devices.component.spec.ts │ │ │ │ └── devices.component.html │ │ │ ├── overview │ │ │ │ ├── components │ │ │ │ │ ├── device-range │ │ │ │ │ │ ├── device-range.component.scss │ │ │ │ │ │ ├── device-range.component.html │ │ │ │ │ │ └── device-range.component.spec.ts │ │ │ │ │ ├── status-history │ │ │ │ │ │ ├── status-history.component.scss │ │ │ │ │ │ ├── status-history.component.html │ │ │ │ │ │ ├── status-history.component.spec.ts │ │ │ │ │ │ └── status-history.component.ts │ │ │ │ │ └── incidents │ │ │ │ │ │ ├── incidents.component.scss │ │ │ │ │ │ ├── incidents.component.spec.ts │ │ │ │ │ │ ├── incident-list │ │ │ │ │ │ ├── incident-list.component.scss │ │ │ │ │ │ ├── incident-list.component.spec.ts │ │ │ │ │ │ ├── incident-list.component.ts │ │ │ │ │ │ └── incident-list.component.html │ │ │ │ │ │ ├── incidents.component.html │ │ │ │ │ │ └── incidents.component.ts │ │ │ │ ├── overview.component.html │ │ │ │ ├── overview.component.spec.ts │ │ │ │ ├── overview.component.ts │ │ │ │ └── overview.component.scss │ │ │ └── settings │ │ │ │ └── settings │ │ │ │ ├── settings.component.spec.ts │ │ │ │ ├── settings.component.scss │ │ │ │ ├── settings.component.ts │ │ │ │ └── settings.component.html │ │ ├── app.component.scss │ │ ├── app.routes.ts │ │ ├── shared │ │ │ ├── services │ │ │ │ ├── shared.service.spec.ts │ │ │ │ ├── media-query.service.spec.ts │ │ │ │ ├── media-query.service.ts │ │ │ │ └── shared.service.ts │ │ │ ├── utlis.ts │ │ │ └── models │ │ │ │ └── types.ts │ │ ├── app.config.ts │ │ ├── app.component.spec.ts │ │ └── app.component.ts │ ├── index.html │ └── main.ts │ ├── public │ └── favicon.ico │ ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json │ ├── .editorconfig │ ├── tsconfig.app.json │ ├── scripts │ └── copy-theme.mjs │ ├── tsconfig.spec.json │ ├── README.md │ ├── .gitignore │ ├── tsconfig.json │ ├── eslint.config.js │ ├── karma.config.js │ ├── package.json │ ├── playwright.config.ts │ ├── e2e │ └── devices.e2e.ts │ └── angular.json ├── .editorconfig ├── pnpm-workspace.yaml ├── MAINTAINERS.md ├── .vscode ├── extensions.json └── settings.json ├── SECURITY.md ├── tools └── theme-download │ ├── package.json │ └── main.ts ├── .gitignore ├── turbo.json ├── .github ├── dependabot.yml └── workflows │ ├── actions │ └── install │ │ └── action.yml │ ├── deploy-pages.yml │ └── build.yml ├── package.json ├── LICENSE.md ├── ix-starter.code-workspace ├── README.md ├── CODE_OF_CONDUCT.md └── CONTRIBUTING.md /.env.production: -------------------------------------------------------------------------------- 1 | BRAND_VERSION=1.0.0 -------------------------------------------------------------------------------- /apps/react-starter/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /apps/angular-starter/src/assets/theme/theme.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_size = 2 6 | max_line_length = 100 7 | -------------------------------------------------------------------------------- /apps/angular-starter/src/environments/environments.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | BRAND_THEME: false 3 | }; 4 | -------------------------------------------------------------------------------- /apps/angular-starter/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemens/ix-starter/HEAD/apps/angular-starter/public/favicon.ico -------------------------------------------------------------------------------- /apps/angular-starter/src/assets/images/brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemens/ix-starter/HEAD/apps/angular-starter/src/assets/images/brand.png -------------------------------------------------------------------------------- /apps/angular-starter/src/assets/images/classic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemens/ix-starter/HEAD/apps/angular-starter/src/assets/images/classic.png -------------------------------------------------------------------------------- /apps/react-starter/src/pages/user-settings/brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemens/ix-starter/HEAD/apps/react-starter/src/pages/user-settings/brand.png -------------------------------------------------------------------------------- /apps/react-starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/user-settings/classic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/siemens/ix-starter/HEAD/apps/react-starter/src/pages/user-settings/classic.png -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | - "tools/*" 5 | 6 | minimumReleaseAge: 2880 7 | minimumReleaseAgeExclude: 8 | - "@siemens/*" 9 | -------------------------------------------------------------------------------- /apps/angular-starter/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | Lukas Maurer 8 | Daniel Leroux 9 | -------------------------------------------------------------------------------- /apps/angular-starter/src/styles.scss: -------------------------------------------------------------------------------- 1 | // Import the corporate theme if present (copied to assets/theme by prebuild script) 2 | @use "assets/theme/theme.scss" as *; 3 | 4 | @import "@siemens/ix/dist/siemens-ix/siemens-ix.css"; 5 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/delete-modal/delete-modal.component.scss: -------------------------------------------------------------------------------- 1 | /* Delete modal component styles - using default IX component styling to match React */ 2 | 3 | ix-modal-content { 4 | margin-left: 3rem; 5 | } 6 | -------------------------------------------------------------------------------- /apps/react-starter/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | width: 100vw; 4 | height: 100vh; 5 | margin: 0; 6 | } 7 | 8 | #root { 9 | width: 100vw; 10 | height: 100vh; 11 | } 12 | 13 | ix-content { 14 | min-width: 20rem; 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "firsttris.vscode-jest-runner", 5 | "dbaeumer.vscode-eslint", 6 | "EditorConfig.EditorConfig", 7 | "psioniq.psi-header" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/add-device-model/add-device-model.component.scss: -------------------------------------------------------------------------------- 1 | .form-grid { 2 | display: grid; 3 | grid-template-columns: repeat(2, 1fr); 4 | gap: 16px; 5 | } 6 | 7 | .item-full-width { 8 | grid-column: 1 / 3 !important; 9 | width: 100%; 10 | } 11 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/device-cell-renderer/device-cell-renderer.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{status}} 5 | 6 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/device-range/device-range.component.scss: -------------------------------------------------------------------------------- 1 | .device-range { 2 | width: 100%; 3 | height: 21rem; 4 | min-height: 21rem; 5 | max-height: 21rem; 6 | } 7 | 8 | .echarts { 9 | min-height: 0; 10 | position: relative; 11 | width: 100%; 12 | height: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/action-cell-renderer/action-cell-renderer.component.scss: -------------------------------------------------------------------------------- 1 | .ix-row { 2 | position: relative; 3 | display: flex; 4 | justify-content: flex-start; 5 | height: 100%; 6 | align-items: center; 7 | } 8 | 9 | .dropdown { 10 | pointer-events: all; 11 | position: absolute; 12 | } 13 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Reporting Security Issues 8 | 9 | If you believe you have found a security vulnerability in iX, we encourage you to let us know right away. We will investigate all legitimate reports and do our best to quickly fix the problem. 10 | -------------------------------------------------------------------------------- /apps/react-starter/README.md: -------------------------------------------------------------------------------- 1 | # `@siemens/ix-react` + React + Vite 2 | 3 | ## Development 4 | 5 | Execute `pnpm dev` 6 | 7 | ## Build 8 | 9 | Execute `pnpm build` 10 | 11 | ## Testing 12 | 13 | ### Unit tests (vitest + @testing-library/react) 14 | 15 | Execute `pnpm test` 16 | 17 | ### E2E tests (playwright) 18 | 19 | Execute `pnpm e2e` 20 | -------------------------------------------------------------------------------- /apps/react-starter/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2025 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | /// 11 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .application-settings { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1.5rem; 5 | } 6 | 7 | .headline { 8 | margin-bottom: 0.75rem; 9 | } 10 | 11 | .upload { 12 | width: fit-content; 13 | width: 25rem; 14 | max-width: 25rem; 15 | } 16 | 17 | ix-content { 18 | min-width: 20rem; 19 | } -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/status-history/status-history.component.scss: -------------------------------------------------------------------------------- 1 | .status-history { 2 | width: 100%; 3 | height: 21rem; 4 | min-height: 21rem; 5 | max-height: 21rem; 6 | } 7 | 8 | .echarts { 9 | position: relative; 10 | width: 100%; 11 | min-height: 0; 12 | height: 100%; 13 | padding-top: 1rem; 14 | } 15 | -------------------------------------------------------------------------------- /apps/react-starter/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | /// 10 | 11 | import "@testing-library/jest-dom/vitest"; 12 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/overview.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |
6 | 7 | 8 |
9 | 10 |
11 |
-------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/device-cell-renderer/device-cell-renderer.component.scss: -------------------------------------------------------------------------------- 1 | .device-row { 2 | height: 100%; 3 | align-items: center; 4 | overflow: hidden; 5 | display: flex; 6 | flex-direction: row; 7 | flex-wrap: nowrap; 8 | gap: 0.5rem; 9 | } 10 | 11 | .device-name { 12 | text-overflow: ellipsis; 13 | white-space: nowrap; 14 | overflow: hidden; 15 | } 16 | -------------------------------------------------------------------------------- /apps/angular-starter/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | ij_typescript_use_double_quotes = false 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/device-range/device-range.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ "device-status.title" | translate }} 5 | 6 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/status-history/status-history.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ "status-history.title" | translate }} 5 | 6 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /tools/theme-download/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ix-theme-downloader", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "download": "tsx main.ts" 6 | }, 7 | "type": "module", 8 | "dependencies": { 9 | "axios": "^1.11.0", 10 | "fs-extra": "^11.2.0", 11 | "tar": "^7.4.3", 12 | "zlib": "^1.0.5" 13 | }, 14 | "devDependencies": { 15 | "@dotenvx/dotenvx": "^1.51.1", 16 | "@types/fs-extra": "^11.0.4", 17 | "tsx": "^4.20.6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/angular-starter/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Siemens iX - Angular Starter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/react-starter/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | *.tsbuildinfo 26 | /test-results/ 27 | /playwright-report/ 28 | /blob-report/ 29 | /playwright/.cache/ 30 | public/theme 31 | -------------------------------------------------------------------------------- /apps/angular-starter/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/app", 7 | "types": [] 8 | }, 9 | "files": [ 10 | "src/main.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/overview/components/device-range/styles.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | .DeviceRange { 11 | height: 21rem; 12 | min-height: 21rem; 13 | max-height: 21rem; 14 | } 15 | 16 | .echarts { 17 | position: relative; 18 | width: 100%; 19 | height: 100%; 20 | } 21 | -------------------------------------------------------------------------------- /apps/react-starter/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Siemens Industrial Experience - React starter 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/overview/styles.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | .Content { 11 | padding-right: 2rem; 12 | } 13 | 14 | .List { 15 | display: flex; 16 | flex-direction: column; 17 | gap: 2rem; 18 | overflow: hidden; 19 | } 20 | 21 | .List.h-100 { 22 | height: 100%; 23 | } 24 | -------------------------------------------------------------------------------- /apps/angular-starter/scripts/copy-theme.mjs: -------------------------------------------------------------------------------- 1 | import fs from "fs-extra"; 2 | import path from "path"; 3 | 4 | try { 5 | const themePackage = import.meta.resolve("@siemens-ix/corporate-theme"); 6 | const theme = path.join(themePackage, "..", ".."); 7 | fs.copySync(theme, path.join(__dirname, "../src/assets/theme"), { 8 | filter: (src) => !src.includes("corporate-theme-alternative/node_modules"), 9 | }); 10 | console.log("Load additional theme"); 11 | } catch (e) { 12 | console.log("No additional theme found", e); 13 | } 14 | -------------------------------------------------------------------------------- /apps/angular-starter/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/spec", 7 | "types": [ 8 | "jasmine" 9 | ] 10 | }, 11 | "include": [ 12 | "src/**/*.spec.ts", 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { OverviewComponent } from './pages/overview/overview.component'; 3 | import { DevicesComponent } from './pages/devices/devices.component'; 4 | 5 | export const routes: Routes = [ 6 | { 7 | path: '', 8 | redirectTo: 'overview', 9 | pathMatch: 'full', 10 | }, 11 | { 12 | path: 'overview', 13 | component: OverviewComponent, 14 | }, 15 | { 16 | path: 'devices', 17 | component: DevicesComponent, 18 | }, 19 | ]; 20 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/devices/components/modal/styles.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | .FormGrid { 11 | display: grid; 12 | grid-template-columns: repeat(2, 1fr); 13 | gap: 16px; 14 | } 15 | 16 | .ItemFullWidth { 17 | grid-column: 1 / 3; 18 | } 19 | 20 | .ModalFooter { 21 | margin-top: 1rem; 22 | } 23 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/overview/components/status-history/styles.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | .StatusHistory { 11 | height: 21rem; 12 | min-height: 21rem; 13 | max-height: 21rem; 14 | } 15 | 16 | .echarts { 17 | position: relative; 18 | width: 100%; 19 | height: 100%; 20 | padding-top: 1rem; 21 | } 22 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/overview/components/incidents/overview/incident.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | export type Incident = { 10 | id: number; 11 | incidentName: string; 12 | icon: string; 13 | infoText: string; 14 | deviceName: string; 15 | ipAddress: string; 16 | date: string; 17 | time: string; 18 | color: string; 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | .idea 5 | node_modules 6 | .pnp 7 | .pnp.js 8 | 9 | # testing 10 | coverage 11 | 12 | # next.js 13 | .next/ 14 | out/ 15 | build 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # turbo 34 | .turbo 35 | 36 | # vercel 37 | .vercel 38 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/devices/components/modal/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import { showModal } from "@siemens/ix-react"; 11 | 12 | import AddDeviceModal from "./add-device-modal"; 13 | 14 | export default async function show() { 15 | await showModal({ 16 | size: "600", 17 | content: , 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/application-settings/styles.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | .ApplicationSettings { 11 | display: flex; 12 | flex-direction: column; 13 | gap: 1.5rem; 14 | } 15 | 16 | .Headline { 17 | margin-bottom: 0.75rem; 18 | } 19 | 20 | .Upload { 21 | width: fit-content; 22 | width: 25rem; 23 | max-width: 25rem; 24 | } 25 | -------------------------------------------------------------------------------- /apps/angular-starter/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/shared/services/shared.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | import { SharedService } from './shared.service'; 4 | 5 | describe('SharedService', () => { 6 | let service: SharedService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ 10 | imports: [TranslateModule.forRoot()] 11 | }); 12 | service = TestBed.inject(SharedService); 13 | }); 14 | 15 | it('should be created', () => { 16 | expect(service).toBeTruthy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalEnv": ["CI", "REACT_BASE", "BRAND_URL", "BRAND_VERSION"], 4 | "globalDependencies": ["**/.env.*local"], 5 | "tasks": { 6 | "build": { 7 | "dependsOn": ["^build"], 8 | "outputs": [".next/**", "!.next/cache/**"] 9 | }, 10 | "dev": { 11 | "persistent": true, 12 | "cache": true 13 | }, 14 | "lint": {}, 15 | "test": { 16 | "cache": false, 17 | "persistent": true 18 | }, 19 | "e2e": { 20 | "cache": false, 21 | "persistent": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/incidents/incidents.component.scss: -------------------------------------------------------------------------------- 1 | .incidents { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1rem; 5 | height: 100%; 6 | overflow: hidden; 7 | } 8 | 9 | .search-and-filter { 10 | display: flex; 11 | gap: 1rem; 12 | margin-block-end: 0.75rem; 13 | 14 | ix-input-group { 15 | max-width: 12.5rem; 16 | } 17 | } 18 | 19 | .btn-group { 20 | border-radius: 0.375rem; 21 | position: relative; 22 | display: inline-flex; 23 | vertical-align: middle; 24 | } 25 | app-incident-list { 26 | height: 100%; 27 | overflow: auto; 28 | } 29 | -------------------------------------------------------------------------------- /apps/react-starter/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts", "playwright.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/overview/components/overview/styles.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | device-status { 11 | flex-grow: 1; 12 | } 13 | 14 | .CardContainer { 15 | display: flex; 16 | gap: 1rem; 17 | 18 | @media (max-width: 768px) { 19 | flex-direction: column; 20 | } 21 | } 22 | 23 | ix-card { 24 | width: 100%; 25 | flex: 1; 26 | } 27 | 28 | ix-card + ix-card { 29 | flex: 2; 30 | } 31 | -------------------------------------------------------------------------------- /apps/react-starter/src/util/mock-api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | import type { Device } from "../types"; 10 | 11 | export const fetchDataSheet = async (): Promise => { 12 | const res = await fetch(`${import.meta.env.BASE_URL}data.json`); 13 | const text = await res.text(); 14 | 15 | if (res.status !== 200) { 16 | throw Error("Could not load data"); 17 | } 18 | 19 | return JSON.parse(text); 20 | }; 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | versioning-strategy: auto 10 | directory: "/" # Location of package manifests 11 | schedule: 12 | interval: "weekly" 13 | reviewers: 14 | - "danielleroux" 15 | 16 | -------------------------------------------------------------------------------- /apps/react-starter/src/types/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | export type DeviceState = "Online" | "Offline" | "Maintenance" | "Error"; 11 | 12 | export type Device = { 13 | id: string; 14 | deviceName: string; 15 | vendor: string; 16 | description?: string; 17 | status: DeviceState; 18 | articleNumber?: string; 19 | macAddress: string; 20 | ipAddress: string; 21 | firmwareVersion?: string; 22 | serialNumber?: string; 23 | }; 24 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/overview/components/incidents/overview/styles.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | .SearchAndFilter { 11 | display: flex; 12 | gap: 1rem; 13 | margin-block-end: 0.75rem; 14 | align-items: flex-end; 15 | 16 | ix-input-group { 17 | max-width: 12.5rem; 18 | } 19 | } 20 | 21 | .Incidents { 22 | display: flex; 23 | flex-direction: column; 24 | gap: 1rem; 25 | height: 100%; 26 | overflow: hidden; 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ix-starter", 3 | "private": true, 4 | "scripts": { 5 | "build": "turbo run build", 6 | "dev": "turbo run dev", 7 | "lint": "turbo run lint", 8 | "test": "turbo run test", 9 | "e2e": "turbo run e2e", 10 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 11 | }, 12 | "devDependencies": { 13 | "eslint": "^9.12.0", 14 | "prettier": "^3.3.3", 15 | "turbo": "latest", 16 | "ix-theme-downloader": "workspace:*" 17 | }, 18 | "packageManager": "pnpm@10.17.0", 19 | "engines": { 20 | "pnpm": ">=10.x.x", 21 | "node": ">=22.21.x" 22 | }, 23 | "volta": { 24 | "node": "22.21.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "[typescript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "[typescriptreact]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[javascript]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[javascriptreact]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[json]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "[css]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/actions/install/action.yml: -------------------------------------------------------------------------------- 1 | name: "PNPM & Turbo Cache" 2 | description: "PNPM & Turbo Cache" 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Cache turbo build setup 7 | uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 8 | with: 9 | path: .turbo 10 | key: ${{ runner.os }}-turbo-${{ hashFiles('**/pnpm-lock.yaml') }} 11 | restore-keys: | 12 | ${{ runner.os }}-turbo- 13 | 14 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda 15 | name: Install pnpm 16 | with: 17 | run_install: false 18 | 19 | - name: Install dependencies 20 | run: pnpm install 21 | shell: bash 22 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/overview/components/overview/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import style from "./styles.module.css"; 11 | 12 | import DeviceRange from "../device-range"; 13 | import StatusHistory from "../status-history"; 14 | 15 | function Overview() { 16 | return ( 17 |
18 | 19 | 20 |
21 | ); 22 | } 23 | 24 | export default Overview; 25 | -------------------------------------------------------------------------------- /apps/react-starter/src/hooks/theme.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import { themeSwitcher } from "@siemens/ix"; 11 | import { useLayoutEffect, useState } from "react"; 12 | 13 | export const useEChartsTheme = () => { 14 | const [echartsTheme, setEchartsTheme] = useState(themeSwitcher.getCurrentTheme()); 15 | 16 | useLayoutEffect(() => { 17 | const { dispose } = themeSwitcher.themeChanged.on((theme) => setEchartsTheme(theme)); 18 | return dispose; 19 | }, []); 20 | 21 | return echartsTheme; 22 | }; 23 | -------------------------------------------------------------------------------- /apps/angular-starter/README.md: -------------------------------------------------------------------------------- 1 | # `@siemens/ix-angular` + Angular + Angular CLI 2 | 3 | ## A starter template for Angular applications using Siemens Industrial Experience (ix-Angular). 4 | 5 | ## Install dependencies 6 | 7 | Execute pnpm install/npm install 8 | 9 | ## Development 10 | 11 | Execute `pnpm start` 12 | 13 | ## Build 14 | 15 | Execute `pnpm build` 16 | The build artifacts will be stored in the dist/ directory 17 | 18 | ## Testing 19 | 20 | ### Unit Tests (Jasmine + Karma) 21 | 22 | Execute `pnpm test` 23 | 24 | ### E2E tests (playwright) 25 | 26 | Execute `pnpm e2e` 27 | 28 | ### Generate Components/Services 29 | 30 | pnpm ng generate component [name] 31 | 32 | pnpm ng generate service [name] 33 | 34 | pnpm ng generate pipe [name] 35 | -------------------------------------------------------------------------------- /apps/react-starter/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | "paths": { 24 | "@/*": ["./src/*"] 25 | } 26 | }, 27 | "include": ["src", "e2e"] 28 | } 29 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/shared/utlis.ts: -------------------------------------------------------------------------------- 1 | import { showMessage } from '@siemens/ix'; 2 | import { iconInfo } from '@siemens/ix-icons/icons'; 3 | 4 | export function useShowDemoMessage() { 5 | showMessage({ 6 | message: 'This feature is currently unavailable in the demo version.', 7 | icon: iconInfo, 8 | actions: [ 9 | { 10 | id: 'cancel', 11 | text: 'Cancel', 12 | type: 'cancel', 13 | }, 14 | { 15 | id: 'okay', 16 | text: 'OK', 17 | type: 'okay', 18 | }, 19 | ], 20 | messageTitle: 'Demo app', 21 | }); 22 | } 23 | 24 | export function toKebabCase(str: string): string { 25 | return str 26 | .replace(/([a-z])([A-Z])/g, '$1-$2') 27 | .replace(/[\s_]+/g, '-') 28 | .toLowerCase(); 29 | } 30 | -------------------------------------------------------------------------------- /apps/react-starter/src/i18n.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | import i18n from "i18next"; 10 | import { initReactI18next } from "react-i18next"; 11 | 12 | import translationEN from "./locales/en/translation.json"; 13 | import translationDE from "./locales/de/translation.json"; 14 | 15 | const resources = { 16 | en: { 17 | translation: translationEN, 18 | }, 19 | de: { 20 | translation: translationDE, 21 | }, 22 | }; 23 | 24 | i18n.use(initReactI18next).init({ 25 | resources, 26 | lng: "en", 27 | interpolation: { 28 | escapeValue: false, 29 | }, 30 | }); 31 | 32 | export default i18n; 33 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/shared/services/media-query.service.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | 4 | import { TestBed } from '@angular/core/testing'; 5 | import { MediaQueryService } from './media-query.service'; 6 | 7 | describe('MediaQueryService', () => { 8 | let service: MediaQueryService; 9 | 10 | beforeEach(() => { 11 | TestBed.configureTestingModule({}); 12 | service = TestBed.inject(MediaQueryService); 13 | }); 14 | 15 | it('should be created', () => { 16 | expect(service).toBeTruthy(); 17 | }); 18 | 19 | it('should have isMobile$ observable', () => { 20 | expect(service.isMobile$).toBeDefined(); 21 | }); 22 | 23 | it('should have isMobile getter', () => { 24 | expect(typeof service.isMobile).toBe('boolean'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/delete-modal/delete-modal.component.html: -------------------------------------------------------------------------------- 1 | 6 | {{ "device-delete-modal.title" | translate }} 7 | 8 | {{ 9 | "device-delete-modal.content" | translate 10 | }} 11 | 12 | 17 | {{ "device-delete-modal.dismiss" | translate }} 18 | 19 | 25 | {{ "device-delete-modal.close" | translate }} 26 | 27 | 28 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/overview.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | import { OverviewComponent } from './overview.component'; 4 | 5 | describe('OverviewComponent', () => { 6 | let component: OverviewComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [OverviewComponent, TranslateModule.forRoot()] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(OverviewComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/settings/settings/settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | import { SettingsComponent } from './settings.component'; 4 | 5 | describe('SettingsComponent', () => { 6 | let component: SettingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [SettingsComponent, TranslateModule.forRoot()] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(SettingsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/device-cell-renderer/device-cell-renderer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DeviceCellRendererComponent } from './device-cell-renderer.component'; 4 | 5 | describe('DeviceCellRendererComponent', () => { 6 | let component: DeviceCellRendererComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [DeviceCellRendererComponent] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DeviceCellRendererComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/incidents/incidents.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | import { IncidentsComponent } from './incidents.component'; 4 | 5 | describe('IncidentsComponent', () => { 6 | let component: IncidentsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [IncidentsComponent, TranslateModule.forRoot()] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(IncidentsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/device-range/device-range.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | import { DeviceRangeComponent } from './device-range.component'; 4 | 5 | describe('DeviceRangeComponent', () => { 6 | let component: DeviceRangeComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [DeviceRangeComponent, TranslateModule.forRoot()], 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DeviceRangeComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/incidents/incident-list/incident-list.component.scss: -------------------------------------------------------------------------------- 1 | .event-list-header-offset { 2 | padding-left: 1.125rem; 3 | } 4 | 5 | .incident-list { 6 | display: flex; 7 | flex-direction: column; 8 | height: 100%; 9 | overflow: hidden; 10 | } 11 | 12 | .event-list { 13 | overflow: auto; 14 | height: 100%; 15 | position: relative; 16 | display: block; 17 | } 18 | 19 | .no-wrap { 20 | flex-wrap: nowrap; 21 | } 22 | 23 | .info-text { 24 | padding-left: 2.5rem; 25 | overflow: hidden; 26 | text-overflow: ellipsis; 27 | } 28 | 29 | .incident-actions { 30 | display: flex; 31 | flex-wrap: nowrap; 32 | gap: 0.5rem; 33 | justify-content: flex-end; 34 | align-items: center; 35 | } 36 | 37 | .desktop { 38 | display: none; 39 | } 40 | 41 | @media (min-width: 768px) { 42 | .desktop { 43 | display: block; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/status-history/status-history.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | import { StatusHistoryComponent } from './status-history.component'; 4 | 5 | describe('StatusHistoryComponent', () => { 6 | let component: StatusHistoryComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [StatusHistoryComponent, TranslateModule.forRoot()] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(StatusHistoryComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/incidents/incident-list/incident-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | import { IncidentListComponent } from './incident-list.component'; 4 | 5 | describe('IncidentListComponent', () => { 6 | let component: IncidentListComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [IncidentListComponent, TranslateModule.forRoot()] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(IncidentListComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /apps/angular-starter/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | # Playwright 45 | node_modules/ 46 | /test-results/ 47 | /playwright-report/ 48 | /blob-report/ 49 | /playwright/.cache/ 50 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/overview.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IxContent, IxContentHeader } from '@siemens/ix-angular/standalone'; 3 | import { DeviceRangeComponent } from './components/device-range/device-range.component'; 4 | import { StatusHistoryComponent } from './components/status-history/status-history.component'; 5 | import { IncidentsComponent } from './components/incidents/incidents.component'; 6 | import { TranslateModule } from '@ngx-translate/core'; 7 | 8 | @Component({ 9 | selector: 'app-overview', 10 | standalone: true, 11 | imports: [ 12 | IxContentHeader, 13 | DeviceRangeComponent, 14 | StatusHistoryComponent, 15 | IncidentsComponent, 16 | IxContent, 17 | TranslateModule, 18 | ], 19 | templateUrl: './overview.component.html', 20 | styleUrl: './overview.component.scss', 21 | }) 22 | export class OverviewComponent { 23 | } 24 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/shared/models/types.ts: -------------------------------------------------------------------------------- 1 | export interface FilterCriteria { 2 | id: string; 3 | value: any; 4 | operator: string; 5 | } 6 | 7 | export interface DeviceData { 8 | id?: string; 9 | deviceName: string; 10 | status: string; 11 | vendor: string; 12 | deviceType: string; 13 | IPAddress: string; 14 | articleNumber: string; 15 | MACAddress: string; 16 | firmwareVersion: string; 17 | serialNumber: string; 18 | } 19 | 20 | export type DeviceState = 'Online' | 'Offline' | 'Maintenance' | 'Error'; 21 | 22 | export interface Device { 23 | id?: string; 24 | deviceName: string; 25 | vendor: string; 26 | description?: string; 27 | status: DeviceState; 28 | articleNumber?: string; 29 | macAddress: string; 30 | ipAddress: string; 31 | firmwareVersion?: string; 32 | serialNumber?: string; 33 | } 34 | 35 | export enum CopiedDataOperType { 36 | COPY_PASTE = 'copy-paste', 37 | CUT_PASTE = 'cut-paste', 38 | } 39 | -------------------------------------------------------------------------------- /apps/react-starter/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import globals from "globals"; 3 | import reactHooks from "eslint-plugin-react-hooks"; 4 | import reactRefresh from "eslint-plugin-react-refresh"; 5 | import tseslint from "typescript-eslint"; 6 | import eslintConfigPrettier from "eslint-config-prettier"; 7 | 8 | export default tseslint.config( 9 | { ignores: ["dist"] }, 10 | { 11 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 12 | files: ["**/*.{ts,tsx}"], 13 | ignores: ["**/public/**"], 14 | languageOptions: { 15 | ecmaVersion: 2020, 16 | globals: globals.browser, 17 | }, 18 | plugins: { 19 | "react-hooks": reactHooks, 20 | "react-refresh": reactRefresh, 21 | }, 22 | rules: { 23 | ...reactHooks.configs.recommended.rules, 24 | "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], 25 | }, 26 | }, 27 | eslintConfigPrettier, 28 | ); 29 | -------------------------------------------------------------------------------- /apps/react-starter/src/hooks/mediaQuery.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import { useEffect, useState } from "react"; 11 | 12 | export function useMediaQuery(query: string) { 13 | const [matches, setMatches] = useState(false); 14 | useEffect(() => { 15 | const matchQueryList = window.matchMedia(query); 16 | const mediaChanged = (event: MediaQueryListEvent) => { 17 | setMatches(event.matches); 18 | }; 19 | matchQueryList.addEventListener("change", mediaChanged); 20 | 21 | setMatches(matchQueryList.matches); 22 | return () => { 23 | matchQueryList.removeEventListener("change", mediaChanged); 24 | }; 25 | }, [query]); 26 | return matches; 27 | } 28 | 29 | export const useIsMobileViewPort = () => useMediaQuery("(max-width: 48em)"); 30 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/overview/components/incidents/overview/incident-list/styles.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | .NoWrap { 11 | flex-wrap: nowrap; 12 | } 13 | 14 | .InfoText { 15 | padding-left: 2.5rem; 16 | overflow: hidden; 17 | text-overflow: ellipsis; 18 | } 19 | 20 | .IncidentActions { 21 | display: flex; 22 | flex-wrap: nowrap; 23 | gap: 0.5rem; 24 | 25 | justify-content: flex-end; 26 | align-items: center; 27 | } 28 | 29 | .IncidentList { 30 | display: flex; 31 | flex-direction: column; 32 | height: 100%; 33 | overflow: hidden; 34 | } 35 | 36 | .EventList { 37 | overflow: auto; 38 | height: 100%; 39 | position: relative; 40 | display: block; 41 | } 42 | 43 | .EventListHeaderOffset { 44 | padding-left: 1.125rem; 45 | } 46 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/incidents/incidents.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{ "incidents.title" | translate }} 4 | 5 |
6 | 13 | 19 | 20 | 21 |
22 | 23 | {{ "cards" | translate }} 24 | 25 | {{ "list" | translate }} 26 |
27 |
28 | 32 |
33 | -------------------------------------------------------------------------------- /apps/react-starter/src/hooks/demoMessage.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import { showMessage } from "@siemens/ix"; 11 | import { iconInfo } from "@siemens/ix-icons/icons"; 12 | import { useCallback } from "react"; 13 | import { useTranslation } from "react-i18next"; 14 | 15 | export default function useShowDemoMessage() { 16 | const { t } = useTranslation(); 17 | const show = useCallback(() => { 18 | showMessage({ 19 | message: t("demo-message"), 20 | icon: iconInfo, 21 | actions: [ 22 | { 23 | id: "cancel", 24 | text: t("cancel"), 25 | type: "cancel", 26 | }, 27 | { 28 | id: "okay", 29 | text: t("okay"), 30 | type: "okay", 31 | }, 32 | ], 33 | messageTitle: "Demo app", 34 | }); 35 | }, [t]); 36 | 37 | return show; 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Siemens AG 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /apps/angular-starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "outDir": "./dist/out-tsc", 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "bundler", 17 | "importHelpers": true, 18 | "target": "ES2022", 19 | "module": "ES2022", 20 | "types": ["node", "@playwright/test"] 21 | }, 22 | "angularCompilerOptions": { 23 | "enableI18nLegacyMessageIdFormat": false, 24 | "strictInjectionParameters": true, 25 | "strictInputAccessModifiers": true, 26 | "strictTemplates": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/devices/components/device-details/styles.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | .Pane { 11 | position: absolute; 12 | right: 0; 13 | top: 0; 14 | z-index: 10; 15 | } 16 | 17 | .Container { 18 | height: 100%; 19 | width: 100%; 20 | display: flex; 21 | flex-direction: column; 22 | justify-content: space-between; 23 | overflow: hidden; 24 | word-break: break-word; 25 | padding: 0 0.75rem 1rem; 26 | } 27 | 28 | .MessageBar { 29 | margin-bottom: 0.25rem; 30 | } 31 | 32 | .MessageBarContent { 33 | display: flex; 34 | align-items: center; 35 | justify-content: space-between; 36 | } 37 | 38 | .ButtonMargin { 39 | margin-left: 0.75rem; 40 | } 41 | 42 | .Divider { 43 | margin: 0.5rem 0; 44 | } 45 | 46 | .DeviceName { 47 | margin-bottom: 2rem; 48 | overflow: hidden; 49 | text-wrap: nowrap; 50 | text-overflow: ellipsis; 51 | } 52 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/overview/components/incidents/overview/overview.test.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | import { expect, test } from "vitest"; 10 | import { render } from "vitest-browser-react"; 11 | import { userEvent } from "@vitest/browser/context"; 12 | import { screen } from "shadow-dom-testing-library"; 13 | import IncidentsOverview from "./index"; 14 | 15 | test(`add filter input`, async () => { 16 | const { getByTestId } = render(); 17 | const filter = screen.getByLabelText("Filter devices"); 18 | await expect.element(filter).toBeInTheDocument(); 19 | await expect.element(filter).toHaveClass(/hydrated/); 20 | 21 | const input = filter.shadowRoot!.querySelector("input")!; 22 | await userEvent.fill(input, "robo1-net-sw17"); 23 | 24 | const items = getByTestId("incident-item").all(); 25 | expect(items).toHaveLength(1); 26 | }); 27 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/devices/styles.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | .DeviceTable { 10 | display: flex; 11 | position: relative; 12 | flex-direction: column; 13 | height: 100%; 14 | } 15 | 16 | .DeviceFilter { 17 | display: flex; 18 | flex-wrap: wrap; 19 | flex-direction: row; 20 | margin-bottom: 2rem; 21 | gap: 1rem; 22 | } 23 | 24 | .DeviceFilter .CategoryFilter { 25 | min-width: 16rem; 26 | flex-grow: 1; 27 | } 28 | 29 | .DeviceFilter .QuickFilter { 30 | display: flex; 31 | flex-wrap: nowrap; 32 | gap: 0.5rem; 33 | min-width: 38rem; 34 | } 35 | 36 | @media only screen and (max-width: 48em) { 37 | .DeviceFilter { 38 | flex-direction: column; 39 | } 40 | 41 | .DeviceFilter .CategoryFilter { 42 | min-width: 0rem; 43 | width: 100%; 44 | } 45 | 46 | .DeviceFilter .QuickFilter { 47 | flex-wrap: wrap; 48 | min-width: 0rem; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/shared/services/media-query.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject, fromEvent, map, startWith } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class MediaQueryService { 8 | private readonly mediaQuery = window.matchMedia('(max-width: 767px)'); 9 | private readonly isMobileSubject = new BehaviorSubject(this.mediaQuery.matches); 10 | 11 | public isMobile$ = this.isMobileSubject.asObservable(); 12 | 13 | constructor() { 14 | this.initializeMediaQuery(); 15 | } 16 | 17 | private initializeMediaQuery() { 18 | // Listen for media query changes 19 | fromEvent(this.mediaQuery, 'change') 20 | .pipe( 21 | map((event: any) => event.matches), 22 | startWith(this.mediaQuery.matches) 23 | ) 24 | .subscribe(matches => { 25 | this.isMobileSubject.next(matches); 26 | }); 27 | } 28 | 29 | get isMobile(): boolean { 30 | return this.isMobileSubject.value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/shared/services/shared.service.ts: -------------------------------------------------------------------------------- 1 | import { inject, Injectable } from '@angular/core'; 2 | import { TranslateService } from '@ngx-translate/core'; 3 | import { ToastService, ToastType } from '@siemens/ix-angular'; 4 | import { BehaviorSubject } from 'rxjs'; 5 | 6 | @Injectable({ 7 | providedIn: 'root', 8 | }) 9 | export class SharedService { 10 | canPasteSubject = new BehaviorSubject(false); 11 | currentLang = new BehaviorSubject('en'); 12 | currentLang$ = this.currentLang.asObservable(); 13 | 14 | private readonly toastService = inject(ToastService); 15 | private readonly translate = inject(TranslateService); 16 | 17 | constructor( 18 | ) { 19 | const savedLang = localStorage.getItem('selectedLang') || 'en'; 20 | this.setLanguage(savedLang); 21 | } 22 | 23 | showToast(message: string, type: ToastType) { 24 | this.toastService.show({ 25 | message, 26 | type, 27 | }); 28 | } 29 | setLanguage(lang: string) { 30 | this.translate.use(lang); 31 | this.currentLang.next(lang); 32 | localStorage.setItem('selectedLang', lang); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/angular-starter/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /apps/react-starter/src/util/util.ts: -------------------------------------------------------------------------------- 1 | import { showToast } from "@siemens/ix-react"; 2 | import { iconSingleCheck } from "@siemens/ix-icons/icons"; 3 | import { MutableRefObject, useEffect } from "react"; 4 | import EChartsReact from "echarts-for-react"; 5 | 6 | export function toKebabCase(normalString: string): string { 7 | return normalString 8 | .replace(/([a-z])([A-Z])/g, "$1-$2") 9 | .replace(/\s+/g, "-") 10 | .toLowerCase() 11 | .replace(/-+/g, "-") 12 | .replace(/^-+|-+$/g, ""); 13 | } 14 | 15 | export function showSuccessToast(message: string) { 16 | showToast({ 17 | message: message, 18 | icon: iconSingleCheck, 19 | iconColor: "color-success", 20 | }); 21 | } 22 | 23 | export function useResizeHandler(chartRef: MutableRefObject) { 24 | useEffect(() => { 25 | const handleResize = () => { 26 | if (chartRef.current) { 27 | chartRef.current.getEchartsInstance().resize(); 28 | } 29 | }; 30 | 31 | window.addEventListener("resize", handleResize); 32 | 33 | return () => { 34 | window.removeEventListener("resize", handleResize); 35 | }; 36 | }, [chartRef]); 37 | } 38 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/settings/settings/settings.component.scss: -------------------------------------------------------------------------------- 1 | .user-settings { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1.5rem; 5 | } 6 | 7 | .themeTitle { 8 | padding-bottom: 1.5rem; 9 | } 10 | 11 | .theme-button { 12 | display: inline-flex; 13 | flex-direction: column; 14 | position: relative; 15 | 16 | align-items: center; 17 | gap: 1rem; 18 | } 19 | .theme-image-preview { 20 | display: inline-flex; 21 | position: relative; 22 | border-radius: 0.125rem; 23 | border: 1px solid var(--theme-color-6); 24 | padding: 0.5rem; 25 | gap: 1rem; 26 | cursor: pointer; 27 | 28 | width: 208px; 29 | height: 160px; 30 | 31 | img { 32 | object-fit: contain; 33 | height: 100%; 34 | width: 100%; 35 | } 36 | } 37 | .active { 38 | border-color: var(--theme-color-dynamic); 39 | } 40 | 41 | .theme-selection { 42 | display: flex; 43 | flex-wrap: wrap; 44 | position: relative; 45 | 46 | gap: 1rem; 47 | } 48 | 49 | .language-selection { 50 | display: flex; 51 | position: relative; 52 | flex-direction: column; 53 | 54 | gap: 0.5rem; 55 | } 56 | 57 | .headline-language { 58 | margin-bottom: 0.75rem; 59 | } 60 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/overview/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import { IxContentHeader } from "@siemens/ix-react"; 11 | import { useTranslation } from "react-i18next"; 12 | import Incidents from "./components/incidents/overview"; 13 | import Overview from "./components/overview"; 14 | import styles from "./styles.module.css"; 15 | import { useIsMobileViewPort } from "@/hooks/mediaQuery"; 16 | import clsx from "clsx"; 17 | 18 | const OverviewPage = () => { 19 | const { t } = useTranslation(); 20 | const isMobile = useIsMobileViewPort(); 21 | 22 | return ( 23 | <> 24 | 25 |
30 | 31 | 32 |
33 | 34 | ); 35 | }; 36 | 37 | export default OverviewPage; 38 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/overview.component.scss: -------------------------------------------------------------------------------- 1 | .list { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 2rem; 5 | overflow: hidden; 6 | height: 100%; 7 | } 8 | 9 | .card-container { 10 | display: flex; 11 | gap: 1rem; 12 | width: 100%; 13 | box-sizing: border-box; 14 | 15 | @media (max-width: 768px) { 16 | flex-direction: column; 17 | } 18 | } 19 | 20 | ix-card { 21 | flex: 1; 22 | } 23 | 24 | ix-card + ix-card { 25 | flex: 2; 26 | } 27 | 28 | app-incidents { 29 | height: 100%; 30 | overflow: auto; 31 | } 32 | 33 | .card-style { 34 | height: 21rem; 35 | min-height: 21rem; 36 | max-height: 21rem; 37 | } 38 | app-device-range { 39 | flex: 1; 40 | min-width: 0; 41 | width: 100%; 42 | box-sizing: border-box; 43 | } 44 | app-status-history { 45 | flex: 2; 46 | min-width: 0; 47 | width: 100%; 48 | box-sizing: border-box; 49 | } 50 | 51 | @media only screen and (max-width: 768px) { 52 | .list { 53 | height: auto; 54 | width: 100%; 55 | } 56 | 57 | .card-container { 58 | flex-direction: column; 59 | } 60 | 61 | app-device-range, 62 | app-status-history { 63 | flex: none; 64 | width: 100%; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/devices/components/ag-grid-table/styles.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | ix-empty-state { 11 | height: fit-content; 12 | } 13 | 14 | .DeviceRow { 15 | height: 100%; 16 | align-items: center; 17 | overflow: hidden; 18 | display: flex; 19 | flex-direction: row; 20 | flex-wrap: nowrap; 21 | gap: 0.5rem; 22 | } 23 | 24 | .DeviceName { 25 | text-overflow: ellipsis; 26 | white-space: nowrap; 27 | overflow: hidden; 28 | } 29 | 30 | .IxRow { 31 | display: flex; 32 | justify-content: flex-start; 33 | height: 100%; 34 | align-items: center; 35 | } 36 | 37 | .DeviceCell { 38 | display: flex; 39 | align-items: center; 40 | height: 100%; 41 | gap: 0.25rem; 42 | } 43 | 44 | .Indicator { 45 | min-width: 0.4rem; 46 | width: 0.4rem; 47 | height: 100%; 48 | } 49 | 50 | .GridWrapper { 51 | flex-grow: 1; 52 | } 53 | 54 | .EmptyStateWrapper { 55 | display: flex; 56 | flex-grow: 1; 57 | justify-content: center; 58 | align-items: center; 59 | } 60 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/add-device-model/add-device-model.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | import { AddDeviceModelComponent } from './add-device-model.component'; 4 | import { IxActiveModal } from '@siemens/ix-angular/standalone'; 5 | 6 | const mockIxActiveModal = { 7 | close: jasmine.createSpy('close'), 8 | dismiss: jasmine.createSpy('dismiss') 9 | // Add any other methods your component uses 10 | }; 11 | 12 | describe('AddDeviceModelComponent', () => { 13 | let component: AddDeviceModelComponent; 14 | let fixture: ComponentFixture; 15 | 16 | beforeEach(async () => { 17 | await TestBed.configureTestingModule({ 18 | imports: [AddDeviceModelComponent, TranslateModule.forRoot()], 19 | providers: [ 20 | { provide: IxActiveModal, useValue: mockIxActiveModal } 21 | ] 22 | }) 23 | .compileComponents(); 24 | 25 | fixture = TestBed.createComponent(AddDeviceModelComponent); 26 | component = fixture.componentInstance; 27 | fixture.detectChanges(); 28 | }); 29 | 30 | it('should create', () => { 31 | expect(component).toBeTruthy(); 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /ix-starter.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "ix-react-starter", 5 | "path": "./apps/react-starter", 6 | }, 7 | { 8 | "name": "ix-angular-starter", 9 | "path": "./apps/angular-starter", 10 | }, 11 | { 12 | "name": "root", 13 | "path": "./", 14 | }, 15 | ], 16 | "settings": { 17 | "psi-header.config": { 18 | "blankLinesAfter": 0, 19 | "forceToTop": true, 20 | }, 21 | "psi-header.lang-config": [ 22 | { 23 | "language": "vue", 24 | "begin": "", 26 | }, 27 | ], 28 | "psi-header.templates": [ 29 | { 30 | "language": "*", 31 | "template": [ 32 | "SPDX-FileCopyrightText: <> Siemens AG", 33 | "", 34 | "SPDX-License-Identifier: MIT", 35 | "", 36 | "This source code is licensed under the MIT license found in the", 37 | "LICENSE file in the root directory of this source tree.", 38 | ], 39 | }, 40 | ], 41 | "psi-header.changes-tracking": { 42 | "isActive": true, 43 | "enforceHeader": false, 44 | "autoHeader": "autoSave", 45 | "language": "*", 46 | "exclude": ["yaml", "md"], 47 | }, 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApplicationConfig, 3 | importProvidersFrom, 4 | provideZoneChangeDetection, 5 | } from '@angular/core'; 6 | import { provideRouter } from '@angular/router'; 7 | import { IxModule } from '@siemens/ix-angular'; 8 | 9 | import { routes } from './app.routes'; 10 | import { BrowserModule } from '@angular/platform-browser'; 11 | import { HttpClient, provideHttpClient } from '@angular/common/http'; 12 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 13 | import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; 14 | 15 | export function HttpLoaderFactory(http: HttpClient) { 16 | return new TranslateHttpLoader(http, './assets/i18n/', '.json'); 17 | } 18 | 19 | export const appConfig: ApplicationConfig = { 20 | providers: [ 21 | provideZoneChangeDetection({ eventCoalescing: true }), 22 | provideRouter(routes), 23 | provideHttpClient(), 24 | importProvidersFrom( 25 | BrowserModule, 26 | 27 | IxModule.forRoot(), 28 | TranslateModule.forRoot({ 29 | defaultLanguage: 'en', 30 | loader: { 31 | provide: TranslateLoader, 32 | useFactory: HttpLoaderFactory, 33 | deps: [HttpClient], 34 | }, 35 | }), 36 | ), 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /apps/react-starter/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import react from "@vitejs/plugin-react"; 3 | import path from "path"; 4 | import fs from "fs-extra"; 5 | 6 | const base = process.env.REACT_BASE || "/"; 7 | 8 | function checkForAdditionalTheme() { 9 | try { 10 | const themePackage = import.meta.resolve("@siemens-ix/corporate-theme"); 11 | const theme = path.join(themePackage.replace("file://", ""), "..", ".."); 12 | 13 | fs.copySync(theme, path.join(__dirname, "public", "theme"), { 14 | filter: (src) => { 15 | return !src.includes("corporate-theme-alternative/node_modules"); 16 | }, 17 | }); 18 | console.log("Load additional theme"); 19 | } catch (e) { 20 | console.log("No additional theme found", e); 21 | } 22 | } 23 | 24 | checkForAdditionalTheme(); 25 | 26 | // https://vitejs.dev/config/ 27 | export default defineConfig({ 28 | base, 29 | plugins: [react()], 30 | resolve: { 31 | alias: [ 32 | { 33 | find: "@", 34 | replacement: path.resolve(__dirname, "src"), 35 | }, 36 | ], 37 | }, 38 | test: { 39 | setupFiles: "./src/setupTests.ts", 40 | browser: { 41 | enabled: true, 42 | provider: "playwright", 43 | instances: [{ browser: "chromium" }], 44 | }, 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/application-settings/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import { IxToggle, IxTypography, IxUpload } from "@siemens/ix-react"; 11 | import styles from "./styles.module.css"; 12 | import { useTranslation } from "react-i18next"; 13 | 14 | export default function ApplicationSettings() { 15 | const { t } = useTranslation(); 16 | return ( 17 |
18 |
19 | 20 | {t("settings.notification")} 21 | 22 | 23 |
24 |
25 | 26 | {t("settings.import-device")} 27 | 28 | 33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/action-cell-renderer/action-cell-renderer.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ActionCellRendererComponent } from './action-cell-renderer.component'; 4 | import { TranslateModule, TranslateService, TranslateStore } from '@ngx-translate/core'; 5 | 6 | describe('ActionCellRendererComponent', () => { 7 | let component: ActionCellRendererComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | await TestBed.configureTestingModule({ 12 | imports: [ActionCellRendererComponent, TranslateModule.forRoot()], 13 | providers: [ 14 | TranslateService, 15 | TranslateStore 16 | ] 17 | 18 | }) 19 | .compileComponents(); 20 | 21 | fixture = TestBed.createComponent(ActionCellRendererComponent); 22 | component = fixture.componentInstance; 23 | 24 | component.params = { 25 | data: { status: 'Online' }, 26 | node: { id: 1, rowIndex: 0 }, 27 | api: { 28 | getColumnDefs: () => [{ field: 'name' }], 29 | getRowNode: () => ({ rowIndex: 0 }), 30 | }, 31 | }; 32 | fixture.detectChanges(); 33 | 34 | }); 35 | 36 | it('should create', () => { 37 | expect(component).toBeTruthy(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/user-settings/styles.module.css: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | .UserSettings { 11 | display: flex; 12 | flex-direction: column; 13 | gap: 1.5rem; 14 | } 15 | 16 | .ThemeSelection { 17 | display: flex; 18 | flex-wrap: wrap; 19 | position: relative; 20 | 21 | gap: 1rem; 22 | } 23 | 24 | .ThemeButton { 25 | display: inline-flex; 26 | flex-direction: column; 27 | position: relative; 28 | 29 | align-items: center; 30 | gap: 1rem; 31 | } 32 | 33 | .ThemeImagePreview { 34 | display: inline-flex; 35 | position: relative; 36 | border-radius: 0.125rem; 37 | border: 1px solid var(--theme-color-6); 38 | padding: 0.5rem; 39 | gap: 1rem; 40 | cursor: pointer; 41 | 42 | width: 208px; 43 | height: 160px; 44 | } 45 | 46 | .ThemeImagePreview img { 47 | object-fit: contain; 48 | height: 100%; 49 | width: 100%; 50 | } 51 | 52 | .ThemeImagePreview.Active { 53 | border-color: var(--theme-color-dynamic); 54 | } 55 | 56 | .LanguageSelection { 57 | display: flex; 58 | position: relative; 59 | flex-direction: column; 60 | 61 | gap: 0.5rem; 62 | } 63 | 64 | .HeadlineLanguage { 65 | margin-bottom: 0.75rem; 66 | } 67 | -------------------------------------------------------------------------------- /apps/react-starter/src/store/hooks/device.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import { Device, DeviceState } from "@/types"; 11 | import { useEffect, useMemo, useState } from "react"; 12 | import { useDataStore, useOverviewPaneStore } from "../device-store"; 13 | 14 | export const useDeviceStatus = () => { 15 | const { devices } = useDataStore(); 16 | 17 | const deviceState = useMemo(() => { 18 | const status: Record = { 19 | Error: 0, 20 | Maintenance: 0, 21 | Offline: 0, 22 | Online: 0, 23 | }; 24 | 25 | devices.forEach((device) => { 26 | status[device.status] = status[device.status] + 1; 27 | }); 28 | 29 | return status; 30 | }, [devices]); 31 | 32 | return deviceState; 33 | }; 34 | 35 | export const useSelectedDevice = () => { 36 | const { devices } = useDataStore(); 37 | const { selectedDataId } = useOverviewPaneStore(); 38 | 39 | const [device, setDevice] = useState(null); 40 | 41 | useEffect(() => { 42 | const selectedDevice = devices.find((device) => device.id === selectedDataId); 43 | setDevice(selectedDevice ?? null); 44 | }, [devices, selectedDataId]); 45 | 46 | return device; 47 | }; -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/delete-modal/delete-modal.component.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import { Component, inject, Inject } from '@angular/core'; 11 | import { 12 | IxButton, 13 | IxModalContent, 14 | IxModalFooter, 15 | IxModalHeader, 16 | IxActiveModal, 17 | } from '@siemens/ix-angular/standalone'; 18 | import { TranslateModule } from '@ngx-translate/core'; 19 | import { addIcons } from '@siemens/ix-icons'; 20 | import { iconTrashcan } from '@siemens/ix-icons/icons'; 21 | 22 | @Component({ 23 | selector: 'app-delete-modal', 24 | standalone: true, 25 | imports: [ 26 | IxModalHeader, 27 | IxModalContent, 28 | IxModalFooter, 29 | IxButton, 30 | TranslateModule, 31 | ], 32 | templateUrl: './delete-modal.component.html', 33 | styleUrl: './delete-modal.component.scss', 34 | }) 35 | export class DeleteModalComponent { 36 | @Inject(IxActiveModal) readonly activeModal = inject(IxActiveModal); 37 | 38 | constructor() { 39 | addIcons({ 40 | iconTrashcan, 41 | }); 42 | } 43 | 44 | close() { 45 | this.activeModal.close({ deleted: true }); 46 | } 47 | 48 | dismiss() { 49 | this.activeModal.dismiss({ deleted: false }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /apps/react-starter/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/angular-starter/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const eslint = require("@eslint/js"); 3 | const tseslint = require("typescript-eslint"); 4 | const angular = require("angular-eslint"); 5 | const eslintConfigPrettier = require("eslint-config-prettier"); 6 | 7 | module.exports = tseslint.config( 8 | { 9 | files: ["**/*.ts"], 10 | extends: [ 11 | eslint.configs.recommended, 12 | ...tseslint.configs.recommended, 13 | ...tseslint.configs.stylistic, 14 | ...angular.configs.tsRecommended, 15 | ], 16 | processor: angular.processInlineTemplates, 17 | rules: { 18 | "@angular-eslint/directive-selector": [ 19 | "error", 20 | { 21 | type: "attribute", 22 | prefix: "app", 23 | style: "camelCase", 24 | }, 25 | ], 26 | "@angular-eslint/component-selector": [ 27 | "error", 28 | { 29 | type: "element", 30 | prefix: "app", 31 | style: "kebab-case", 32 | }, 33 | ], 34 | "@typescript-eslint/no-explicit-any": "off", 35 | }, 36 | }, 37 | { 38 | files: ["**/*.html"], 39 | extends: [ 40 | ...angular.configs.templateRecommended, 41 | ...angular.configs.templateAccessibility, 42 | ], 43 | rules: { 44 | "@angular-eslint/template/click-events-have-key-events": "off", 45 | "@angular-eslint/template/interactive-supports-focus": "off", 46 | }, 47 | }, 48 | eslintConfigPrettier, 49 | ); 50 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/overview/components/quick-actions/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import useShowDemoMessage from "@/hooks/demoMessage"; 11 | import { IxActionCard } from "@siemens/ix-react"; 12 | import { useTranslation } from "react-i18next"; 13 | import { iconAddCircle, iconList, iconPiechart, iconMaintenance } from "@siemens/ix-icons/icons"; 14 | 15 | function QuickActions() { 16 | const { t } = useTranslation(); 17 | const showDemoMessage = useShowDemoMessage(); 18 | 19 | return ( 20 | <> 21 | 27 | 32 | 37 | 42 | 43 | ); 44 | } 45 | 46 | export default QuickActions; 47 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/devices/components/ag-grid-table/device-cell-renderer.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import { iconError, iconInfo, iconMaintenanceWarning, iconSuccess } from "@siemens/ix-icons/icons"; 11 | import { IxIcon, IxRow, IxTypography } from "@siemens/ix-react"; 12 | import { ICellRendererParams } from "ag-grid-community"; 13 | import { AgGridReact } from "ag-grid-react"; 14 | import { RefObject } from "react"; 15 | import styles from "./styles.module.css"; 16 | 17 | type CustomDeviceCellRendererProps = ICellRendererParams & { 18 | gridRef: RefObject; 19 | }; 20 | 21 | const CustomDeviceCellRenderer = (props: CustomDeviceCellRendererProps) => { 22 | return ( 23 | 24 | {props.data.status === "Online" ? ( 25 | 26 | ) : props.data.status === "Offline" ? ( 27 | 28 | ) : props.data.status === "Maintenance" ? ( 29 | 30 | ) : ( 31 | 32 | )} 33 | {props.data.status} 34 | 35 | ); 36 | }; 37 | 38 | export default CustomDeviceCellRenderer; 39 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/devices/components/ag-grid-table/delete-modal.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import { useRef } from "react"; 11 | import { 12 | IxButton, 13 | IxModalContent, 14 | IxModalFooter, 15 | IxModalHeader, 16 | Modal, 17 | ModalRef, 18 | } from "@siemens/ix-react"; 19 | import { iconTrashcan } from "@siemens/ix-icons/icons"; 20 | import { useTranslation } from "react-i18next"; 21 | 22 | export default function DeleteModal() { 23 | const { t } = useTranslation(); 24 | const modalRef = useRef(null); 25 | 26 | const close = () => { 27 | modalRef.current?.close({ deleted: true }); 28 | }; 29 | const dismiss = () => { 30 | modalRef.current?.dismiss({ deleted: false }); 31 | }; 32 | 33 | return ( 34 | 35 | dismiss()} icon={iconTrashcan} iconColor="color-alarm"> 36 | {t("device-delete-modal.title")} 37 | 38 | {t("device-delete-modal.content")} 39 | 40 | dismiss()}> 41 | {t("device-delete-modal.dismiss")} 42 | 43 | close()}> 44 | {t("device-delete-modal.close")} 45 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/devices.component.scss: -------------------------------------------------------------------------------- 1 | .device-table { 2 | display: flex; 3 | position: relative; 4 | flex-direction: column; 5 | height: 100%; 6 | width: 100%; 7 | } 8 | 9 | .device-filter { 10 | display: flex; 11 | flex-wrap: wrap; 12 | flex-direction: row; 13 | margin-bottom: 2rem; 14 | gap: 1rem; 15 | } 16 | 17 | .device-filter .category-filter { 18 | min-width: 16rem; 19 | flex-grow: 1; 20 | } 21 | 22 | .quick-filter { 23 | display: flex; 24 | flex-wrap: nowrap; 25 | gap: 0.5rem; 26 | min-width: 38rem; 27 | } 28 | 29 | .pane { 30 | position: absolute; 31 | right: 0; 32 | top: 0; 33 | z-index: 10; 34 | } 35 | .container { 36 | height: 100%; 37 | width: 100%; 38 | display: flex; 39 | flex-direction: column; 40 | justify-content: space-between; 41 | overflow: hidden; 42 | word-break: break-word; 43 | padding: 0 0.75rem 1rem; 44 | } 45 | 46 | .empty-state-wrapper { 47 | display: flex; 48 | flex-grow: 1; 49 | justify-content: center; 50 | align-items: center; 51 | } 52 | 53 | .grid-container { 54 | width: 100%; 55 | height: 76vh; 56 | } 57 | 58 | .device-name { 59 | margin-bottom: 2rem; 60 | overflow: hidden; 61 | text-wrap: nowrap; 62 | text-overflow: ellipsis; 63 | } 64 | 65 | @media only screen and (max-width: 48em) { 66 | .device-filter { 67 | flex-direction: column; 68 | } 69 | 70 | .device-filter .category-filter { 71 | min-width: 0rem; 72 | width: 100%; 73 | } 74 | 75 | .device-filter .quick-filter { 76 | flex-wrap: wrap; 77 | min-width: 0rem; 78 | } 79 | .grid-container { 80 | height: 59vh; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /apps/angular-starter/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | import { environment } from './environments/environments'; 5 | import { getIxTheme } from '@siemens/ix-aggrid'; 6 | import * as agGrid from 'ag-grid-community'; 7 | import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'; 8 | 9 | function setupAGGrid() { 10 | ModuleRegistry.registerModules([AllCommunityModule]); 11 | 12 | const ixTheme = getIxTheme(agGrid); 13 | agGrid.provideGlobalGridOptions({ 14 | theme: ixTheme, 15 | }); 16 | } 17 | 18 | setupAGGrid(); 19 | 20 | bootstrapApplication(AppComponent, appConfig).catch((err) => 21 | console.error(err), 22 | ); 23 | 24 | // Dynamically load the corporate theme if present 25 | // Only needed for deployments of demo 26 | function optionalTheme() { 27 | if (environment.BRAND_THEME) { 28 | const baseHref = document.querySelector('base')?.href || '/'; 29 | const css = `${baseHref}assets/theme/dist/css/brand-theme.css`; 30 | const link = document.createElement('link'); 31 | link.rel = 'stylesheet'; 32 | link.href = css; 33 | document.head.appendChild(link); 34 | const loader = `${baseHref}assets/theme/dist/index.js`; 35 | const script = document.createElement('script'); 36 | script.src = loader; 37 | script.type = 'module'; 38 | document.head.appendChild(script); 39 | document.documentElement.setAttribute('data-ix-theme', 'brand'); 40 | document.documentElement.setAttribute('data-ix-color-schema', 'dark'); 41 | } 42 | } 43 | 44 | optionalTheme(); 45 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/device-cell-renderer/device-cell-renderer.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IxIcon, IxRow, IxTypography } from '@siemens/ix-angular/standalone'; 3 | import { addIcons } from '@siemens/ix-icons'; 4 | import { iconSuccess } from '@siemens/ix-icons/icons'; 5 | import { ICellRendererAngularComp } from 'ag-grid-angular'; 6 | 7 | @Component({ 8 | selector: 'app-device-cell-renderer', 9 | imports: [IxRow, IxTypography, IxIcon], 10 | standalone: true, 11 | templateUrl: './device-cell-renderer.component.html', 12 | styleUrl: './device-cell-renderer.component.scss' 13 | }) 14 | export class DeviceCellRendererComponent implements ICellRendererAngularComp { 15 | params: any; 16 | iconName = "success"; 17 | iconColor = "color-success"; 18 | status = "Online"; 19 | 20 | constructor() { 21 | addIcons({ 22 | iconSuccess, 23 | }); 24 | } 25 | 26 | agInit(params: any): void { 27 | this.status = params.data.status; 28 | 29 | switch (params.data.status) { 30 | case "Online": 31 | this.iconName = "success"; 32 | this.iconColor = "color-success"; 33 | break; 34 | case "Offline": 35 | this.iconName = "info"; 36 | this.iconColor = ""; 37 | break; 38 | case "Maintenance": 39 | this.iconName = "maintenance-warning"; 40 | this.iconColor = "color-warning"; 41 | break; 42 | default: 43 | this.iconName = "error"; 44 | this.iconColor = "color-alarm"; 45 | break; 46 | } 47 | } 48 | 49 | refresh(params: any): boolean { 50 | this.params = params; 51 | return true; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /apps/angular-starter/karma.config.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file for headless testing 2 | module.exports = function (config) { 3 | config.set({ 4 | basePath: '', 5 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 6 | plugins: [ 7 | require('karma-jasmine'), 8 | require('karma-chrome-launcher'), 9 | require('karma-jasmine-html-reporter'), 10 | require('@angular-devkit/build-angular/plugins/karma') 11 | ], 12 | reporters: ['progress'], 13 | port: 9876, 14 | colors: true, 15 | logLevel: config.LOG_INFO, 16 | autoWatch: true, 17 | browsers: ['ChromeHeadless'], 18 | 19 | customLaunchers: { 20 | ChromeHeadlessCustom: { 21 | base: 'ChromeHeadless', 22 | flags: [ 23 | '--no-sandbox', 24 | '--disable-gpu', 25 | '--disable-web-security', 26 | '--disable-features=VizDisplayCompositor', 27 | '--remote-debugging-port=9222' 28 | ] 29 | }, 30 | ChromeHeadlessCI: { 31 | base: 'ChromeHeadless', 32 | flags: [ 33 | '--no-sandbox', 34 | '--disable-gpu', 35 | '--disable-web-security', 36 | '--disable-dev-shm-usage', 37 | '--disable-extensions', 38 | '--remote-debugging-port=9222' 39 | ] 40 | } 41 | }, 42 | singleRun: true, 43 | concurrency: Infinity, 44 | browserDisconnectTimeout: 10000, 45 | browserDisconnectTolerance: 3, 46 | browserNoActivityTimeout: 60000, 47 | captureTimeout: 60000, 48 | retryLimit: 3 49 | }); 50 | 51 | if (process.env.CI) { 52 | config.set({ 53 | browsers: ['ChromeHeadlessCI'], 54 | singleRun: true, 55 | autoWatch: false, 56 | concurrency: 1 57 | }); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/incidents/incidents.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 3 | import { IncidentListComponent } from './incident-list/incident-list.component'; 4 | import { 5 | IxTypography, 6 | IxIcon, 7 | IxButton, 8 | IxInput, 9 | } from '@siemens/ix-angular/standalone'; 10 | import { useShowDemoMessage } from '../../../../shared/utlis'; 11 | import { 12 | iconSearch, 13 | iconCardLayout, 14 | iconList, 15 | iconCloudUpload, 16 | iconMaintenanceWarning, 17 | iconInfo, 18 | iconError, 19 | } from '@siemens/ix-icons/icons'; 20 | import { addIcons } from '@siemens/ix-icons'; 21 | import { TranslateModule } from '@ngx-translate/core'; 22 | import { INCIDENTS } from '../../../../../assets/mock-data/Incidents'; 23 | 24 | @Component({ 25 | selector: 'app-incidents', 26 | standalone: true, 27 | imports: [ 28 | IxTypography, 29 | IxInput, 30 | IxIcon, 31 | IxButton, 32 | ReactiveFormsModule, 33 | IncidentListComponent, 34 | TranslateModule, 35 | FormsModule, 36 | ], 37 | templateUrl: './incidents.component.html', 38 | styleUrls: ['./incidents.component.scss'], 39 | }) 40 | export class IncidentsComponent { 41 | searchText = ''; 42 | readonly icons = { 43 | iconSearch, 44 | iconCardLayout, 45 | iconList, 46 | iconCloudUpload, 47 | iconMaintenanceWarning, 48 | iconInfo, 49 | iconError, 50 | }; 51 | openModal() { 52 | useShowDemoMessage(); 53 | } 54 | incidents = INCIDENTS; 55 | constructor() { 56 | addIcons({ iconSearch, iconCardLayout, iconList }); 57 | } 58 | 59 | onSearch(event: CustomEvent) { 60 | this.searchText = event.detail; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/deploy-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy GitHub Page 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | confirm_deployment: 7 | description: "Please confirm deployment" 8 | required: true 9 | type: boolean 10 | 11 | concurrency: deploy-gh-pages-${{ github.ref }} 12 | 13 | jobs: 14 | deploy-gh-pages: 15 | if: ${{ inputs.confirm_deployment == true && github.repository == 'siemens/ix-starter' }} 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v3 20 | 21 | - uses: ./.github/workflows/actions/install 22 | 23 | - name: Install optional theme 24 | run: pnpm --filter ix-theme-downloader download 25 | env: 26 | CSC_TOKEN: ${{ secrets.CSC_TOKEN }} 27 | BRAND_URL: ${{ vars.BRAND_URL }} 28 | 29 | - name: Build React Starter 30 | run: pnpm build --filter ix-react-starter 31 | env: 32 | REACT_BASE: /ix-starter/react-starter/ 33 | VITE_THEME: 1 34 | 35 | - name: Enable Brand Theme in Angular Starter 36 | run: | 37 | cd ./apps/angular-starter 38 | sed -i "s/BRAND_THEME: false/BRAND_THEME: true/" src/environments/environments.ts 39 | 40 | - name: Build Angular Starter 41 | run: pnpm --filter ix-angular-starter run build --base-href /ix-starter/angular-starter/ 42 | 43 | - name: Deploy React App 🚀 44 | uses: JamesIves/github-pages-deploy-action@v4 45 | with: 46 | folder: ./apps/react-starter/dist 47 | target-folder: react-starter 48 | 49 | - name: Deploy Angular App 🚀 50 | uses: JamesIves/github-pages-deploy-action@v4 51 | with: 52 | folder: ./apps/angular-starter/dist/browser 53 | target-folder: angular-starter 54 | -------------------------------------------------------------------------------- /apps/react-starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ix-react-starter", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -p ./tsconfig.app.json && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview", 11 | "test": "vitest", 12 | "e2e": "playwright test" 13 | }, 14 | "dependencies": { 15 | "@siemens/ix": "^4.0.0", 16 | "@siemens/ix-aggrid": "^4.0.0", 17 | "@siemens/ix-echarts": "3.0.0", 18 | "@siemens/ix-icons": "3.2.0", 19 | "@siemens/ix-react": "^4.0.0", 20 | "ag-grid-community": "33.3.2", 21 | "ag-grid-react": "33.3.2", 22 | "clsx": "^2.1.1", 23 | "echarts": "^5.5.1", 24 | "echarts-for-react": "^3.0.2", 25 | "element-internals-polyfill": "^3.0.1", 26 | "i18next": "^25.6.0", 27 | "react": "^18.3.1", 28 | "react-dom": "^18.3.1", 29 | "react-hook-form": "^7.53.0", 30 | "react-i18next": "^15.0.2", 31 | "react-router-dom": "^6.27.0", 32 | "zustand": "5.0.0-rc.2" 33 | }, 34 | "devDependencies": { 35 | "@eslint/js": "^9.9.0", 36 | "@playwright/test": "^1.51.1", 37 | "@testing-library/jest-dom": "^6.5.0", 38 | "@testing-library/react": "^16.0.1", 39 | "@types/fs-extra": "^11.0.4", 40 | "@types/react": "^18.3.12", 41 | "@types/react-dom": "^18.3.0", 42 | "@vitejs/plugin-react": "^4.4.0", 43 | "@vitest/browser": "^3.1.1", 44 | "eslint": "^9.12.0", 45 | "eslint-config-prettier": "^9.1.0", 46 | "eslint-plugin-react": "^7.35.0", 47 | "eslint-plugin-react-hooks": "^5.1.0-rc.0", 48 | "eslint-plugin-react-refresh": "^0.4.14", 49 | "fs-extra": "^11.2.0", 50 | "globals": "^15.9.0", 51 | "prettier": "^3.3.3", 52 | "shadow-dom-testing-library": "^1.12.0", 53 | "typescript": "^5.6.3", 54 | "typescript-eslint": "^8.0.1", 55 | "vite": "^6.3.5", 56 | "vitest": "^3.1.1", 57 | "vitest-browser-react": "^0.1.1" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/settings/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, OnDestroy, OnInit } from '@angular/core'; 2 | import { 3 | IxRadio, 4 | IxRadioGroup, 5 | IxTypography, 6 | } from '@siemens/ix-angular/standalone'; 7 | import { TranslateModule } from '@ngx-translate/core'; 8 | import { useShowDemoMessage } from '../../../shared/utlis'; 9 | import { SharedService } from '../../../shared/services/shared.service'; 10 | import { environment } from '../../../../environments/environments'; 11 | import { themeSwitcher } from '@siemens/ix'; 12 | import { Subscription } from 'rxjs'; 13 | 14 | @Component({ 15 | selector: 'app-settings', 16 | imports: [IxTypography, IxRadio, IxRadioGroup, TranslateModule], 17 | templateUrl: './settings.component.html', 18 | styleUrl: './settings.component.scss', 19 | }) 20 | export class SettingsComponent implements OnInit, OnDestroy { 21 | selectedThemeVariant = 'theme-classic'; 22 | currentLang = 'en'; 23 | isProdMode = false; 24 | private readonly sharedService = inject(SharedService); 25 | 26 | private languageChangeDisposable?: Subscription; 27 | 28 | ngOnInit() { 29 | this.isProdMode = environment.BRAND_THEME; 30 | this.languageChangeDisposable = this.sharedService.currentLang$.subscribe( 31 | (lang) => { 32 | this.currentLang = lang; 33 | }, 34 | ); 35 | } 36 | 37 | ngOnDestroy(): void { 38 | this.languageChangeDisposable?.unsubscribe(); 39 | } 40 | 41 | switchLanguage(lang: string) { 42 | this.sharedService.setLanguage(lang); 43 | } 44 | 45 | onThemeChange(selectedThemeVariant: string) { 46 | if (environment.BRAND_THEME) { 47 | const currenttheme = themeSwitcher.getCurrentTheme(); 48 | const newTheme = currenttheme.includes('light') 49 | ? selectedThemeVariant + '-light' 50 | : selectedThemeVariant + '-dark'; 51 | themeSwitcher.setTheme(newTheme); 52 | 53 | this.selectedThemeVariant = selectedThemeVariant; 54 | } else { 55 | useShowDemoMessage(); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/incidents/incident-list/incident-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, inject } from '@angular/core'; 2 | import { 3 | iconCloudUpload, 4 | iconMaintenanceWarning, 5 | iconInfo, 6 | iconError, 7 | iconOpenExternal, 8 | iconUpload, 9 | } from '@siemens/ix-icons/icons'; 10 | import { addIcons } from '@siemens/ix-icons'; 11 | import { 12 | IxLayoutGrid, 13 | IxRow, 14 | IxCol, 15 | IxEventList, 16 | IxEventListItem, 17 | IxTypography, 18 | IxIcon, 19 | IxIconButton, 20 | IxButton, 21 | } from '@siemens/ix-angular/standalone'; 22 | import { useShowDemoMessage } from '../../../../../shared/utlis'; 23 | import { TranslateModule } from '@ngx-translate/core'; 24 | import { MediaQueryService } from '../../../../../shared/services/media-query.service'; 25 | import { AsyncPipe } from '@angular/common'; 26 | 27 | @Component({ 28 | selector: 'app-incident-list', 29 | standalone: true, 30 | imports: [ 31 | IxLayoutGrid, 32 | IxRow, 33 | IxCol, 34 | IxEventList, 35 | IxEventListItem, 36 | IxTypography, 37 | IxIcon, 38 | IxIconButton, 39 | IxButton, 40 | TranslateModule, 41 | AsyncPipe, 42 | ], 43 | templateUrl: './incident-list.component.html', 44 | styleUrl: './incident-list.component.scss', 45 | }) 46 | export class IncidentListComponent { 47 | @Input() incidents: any[] = []; 48 | @Input() search = ''; 49 | 50 | public readonly mediaQueryService = inject(MediaQueryService); 51 | 52 | constructor() { 53 | addIcons({ 54 | iconCloudUpload, 55 | iconMaintenanceWarning, 56 | iconInfo, 57 | iconError, 58 | iconOpenExternal, 59 | iconUpload, 60 | }); 61 | } 62 | 63 | usemodal() { 64 | useShowDemoMessage(); 65 | } 66 | get filteredIncidents() { 67 | const query = this.search.toLowerCase(); 68 | return this.incidents.filter((incident) => 69 | Object.values(incident).some( 70 | (value) => 71 | typeof value === 'string' && value.toLowerCase().includes(query), 72 | ), 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/add-device-model/add-device-model.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, Inject, OnInit } from '@angular/core'; 2 | import { 3 | FormControl, 4 | FormGroup, 5 | ReactiveFormsModule, 6 | } from '@angular/forms'; 7 | import { TranslateModule } from '@ngx-translate/core'; 8 | 9 | import { 10 | IxModalHeader, 11 | IxModalContent, 12 | IxModalFooter, 13 | IxButton, 14 | IxActiveModal, 15 | IxInput, 16 | IxTextValueAccessorDirective, 17 | IxSelect, 18 | IxSelectItem, 19 | IxSelectValueAccessorDirective, 20 | } from '@siemens/ix-angular/standalone'; 21 | 22 | @Component({ 23 | selector: 'app-add-device-model', 24 | standalone: true, 25 | imports: [ 26 | IxModalHeader, 27 | IxModalContent, 28 | IxModalFooter, 29 | IxButton, 30 | IxInput, 31 | IxTextValueAccessorDirective, 32 | ReactiveFormsModule, 33 | TranslateModule, 34 | IxSelect, 35 | IxSelectItem, 36 | IxSelectValueAccessorDirective, 37 | ], 38 | templateUrl: './add-device-model.component.html', 39 | styleUrl: './add-device-model.component.scss', 40 | }) 41 | export class AddDeviceModelComponent implements OnInit { 42 | addDeviceForm!: FormGroup; 43 | @Inject(IxActiveModal) readonly activeModal = inject(IxActiveModal); 44 | 45 | ngOnInit() { 46 | this.addDeviceForm = new FormGroup({ 47 | deviceName: new FormControl(''), 48 | vendor: new FormControl(''), 49 | deviceType: new FormControl(''), // ✓ Matches DeviceData.deviceType 50 | status: new FormControl('Online'), // ✓ Matches DeviceData.status 51 | IPAddress: new FormControl(''), // ✓ Matches DeviceData.IPAddress 52 | articleNumber: new FormControl(''), // ✓ Matches DeviceData.articleNumber 53 | MACAddress: new FormControl(''), // Changed from 'macAddress' to 'MACAddress' 54 | firmwareVersion: new FormControl(''), // ✓ Matches DeviceData.firmwareVersion 55 | serialNumber: new FormControl(''), // ✓ Matches DeviceData.serialNumber 56 | }); 57 | } 58 | 59 | submitForm() { 60 | this.activeModal.close(this.addDeviceForm.value); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /apps/angular-starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ix-angular-starter", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "prebuild": "node scripts/copy-theme.mjs", 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "dev": "ng serve", 9 | "build": "ng build", 10 | "watch": "ng build --watch --configuration development", 11 | "test": "ng test", 12 | "lint": "ng lint", 13 | "e2e": "playwright test", 14 | "e2e:ui": "playwright test --ui", 15 | "e2e:debug": "playwright test --debug" 16 | }, 17 | "private": true, 18 | "dependencies": { 19 | "@angular/common": "^19.2.0", 20 | "@angular/compiler": "^19.2.0", 21 | "@angular/core": "^19.2.0", 22 | "@angular/forms": "^19.2.0", 23 | "@angular/platform-browser": "^19.2.0", 24 | "@angular/platform-browser-dynamic": "^19.2.0", 25 | "@angular/router": "^19.2.0", 26 | "@ngx-translate/core": "^16.0.4", 27 | "@ngx-translate/http-loader": "^16.0.1", 28 | "@siemens/ix": "^4.0.0", 29 | "@siemens/ix-aggrid": "^4.0.0", 30 | "@siemens/ix-angular": "^4.0.0", 31 | "@siemens/ix-echarts": "^3.0.0", 32 | "@siemens/ix-icons": "^3.2.0", 33 | "ag-grid-angular": "^34.3.1", 34 | "ag-grid-community": "^34.3.1", 35 | "echarts": "5.4.3", 36 | "echarts-gl": "^2.0.9", 37 | "ngx-echarts": "^18.0.0", 38 | "rxjs": "~7.8.0", 39 | "tslib": "^2.3.0", 40 | "zone.js": "~0.15.0" 41 | }, 42 | "devDependencies": { 43 | "@angular-devkit/build-angular": "^19.2.8", 44 | "@angular/cli": "^19.2.8", 45 | "@angular/compiler-cli": "^19.2.0", 46 | "@playwright/test": "^1.51.1", 47 | "@types/fs-extra": "^11.0.4", 48 | "@types/jasmine": "~5.1.0", 49 | "@types/node": "^24.0.4", 50 | "angular-eslint": "19.8.1", 51 | "fs-extra": "^11.3.1", 52 | "jasmine-core": "~5.6.0", 53 | "karma": "~6.4.0", 54 | "karma-chrome-launcher": "~3.2.0", 55 | "karma-coverage": "~2.2.0", 56 | "karma-jasmine": "~5.1.0", 57 | "karma-jasmine-html-reporter": "~2.1.0", 58 | "typescript": "~5.7.2" 59 | }, 60 | "peerDependencies": { 61 | "@angular/core": "^19.2.0", 62 | "@angular/forms": "^19.2.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/settings/settings/settings.component.html: -------------------------------------------------------------------------------- 1 | 65 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { AppComponent } from './app.component'; 3 | import { TranslateModule } from '@ngx-translate/core'; 4 | import { fakeAsync, tick, TestBed } from '@angular/core/testing'; 5 | import { NavigationEnd, provideRouter, Router } from '@angular/router'; 6 | import { Subject } from 'rxjs'; 7 | 8 | describe('AppComponent', () => { 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | imports: [AppComponent, TranslateModule.forRoot()], 12 | providers: [ 13 | provideRouter([]), 14 | ], 15 | }).compileComponents(); 16 | }); 17 | 18 | it('should create the app', () => { 19 | const fixture = TestBed.createComponent(AppComponent); 20 | const app = fixture.componentInstance; 21 | expect(app).toBeTruthy(); 22 | }); 23 | describe('AppComponent additional logic', () => { 24 | let fixture: any; 25 | let component: AppComponent; 26 | let router: Router; 27 | 28 | 29 | beforeEach(() => { 30 | fixture = TestBed.createComponent(AppComponent); 31 | component = fixture.componentInstance; 32 | router = TestBed.inject(Router); 33 | }); 34 | 35 | it('should set activePage to "overview" if url contains "overview"', () => { 36 | component['setActivePageFromUrl']('/overview'); 37 | expect(component.activePage).toBe('overview'); 38 | }); 39 | 40 | it('should set activePage to "devices" if url contains "devices"', () => { 41 | component['setActivePageFromUrl']('/devices'); 42 | expect(component.activePage).toBe('devices'); 43 | }); 44 | 45 | it('should call setActivePageFromUrl on ngOnInit with initial url', () => { 46 | const spy = spyOn(AppComponent.prototype as any, 'setActivePageFromUrl'); 47 | const fixture = TestBed.createComponent(AppComponent); 48 | const component = fixture.componentInstance; 49 | component.ngOnInit(); 50 | expect(spy).toHaveBeenCalledWith('/'); 51 | }); 52 | 53 | it('should update activePage on NavigationEnd event', fakeAsync(() => { 54 | component.ngOnInit(); 55 | (router.events as Subject).next(new NavigationEnd(1, '/devices', '/devices')); 56 | tick(); 57 | expect(component.activePage).toBe('devices'); 58 | })); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /apps/react-starter/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { themeSwitcher } from "@siemens/ix"; 2 | import { getIxTheme } from "@siemens/ix-aggrid"; 3 | import { IxApplicationContext } from "@siemens/ix-react"; 4 | import "@siemens/ix/dist/siemens-ix/siemens-ix-core.css"; 5 | import "@siemens/ix/dist/siemens-ix/theme/classic-dark.css"; 6 | import "@siemens/ix/dist/siemens-ix/theme/classic-light.css"; 7 | import { AllCommunityModule, ModuleRegistry, provideGlobalGridOptions } from "ag-grid-community"; 8 | import { StrictMode } from "react"; 9 | import { createRoot } from "react-dom/client"; 10 | import { createHashRouter, RouterProvider } from "react-router-dom"; 11 | import App from "./App.tsx"; 12 | import "./i18n"; 13 | import "./index.css"; 14 | import DevicesPage from "./pages/devices/index.tsx"; 15 | import OverviewPage from "./pages/overview/index.tsx"; 16 | import * as agGrid from "ag-grid-community"; 17 | 18 | ModuleRegistry.registerModules([AllCommunityModule]); 19 | 20 | function optionalTheme() { 21 | if (import.meta.env.VITE_THEME) { 22 | const css = `${import.meta.env.BASE_URL}theme/dist/css/brand-theme.css`; 23 | const link = document.createElement("link"); 24 | link.rel = "stylesheet"; 25 | link.href = css; 26 | document.head.appendChild(link); 27 | 28 | const loader = `${import.meta.env.BASE_URL}theme/dist/index.js`; 29 | const script = document.createElement("script"); 30 | script.src = loader; 31 | script.type = "module"; 32 | document.head.appendChild(script); 33 | 34 | themeSwitcher.setTheme("brand"); 35 | } 36 | } 37 | 38 | optionalTheme(); 39 | 40 | async function configureAgGridTheme() { 41 | const ixTheme = getIxTheme(agGrid); 42 | provideGlobalGridOptions({ 43 | theme: ixTheme, 44 | }); 45 | } 46 | 47 | // Initialize AG Grid theme and then render the app 48 | configureAgGridTheme().then(() => { 49 | const router = createHashRouter([ 50 | { 51 | path: "/", 52 | element: , 53 | children: [ 54 | { 55 | path: "/", 56 | element: , 57 | }, 58 | { 59 | path: "/devices", 60 | element: , 61 | }, 62 | ], 63 | }, 64 | ]); 65 | 66 | createRoot(document.getElementById("root")!).render( 67 | 68 | 69 | 70 | 71 | , 72 | ); 73 | }); 74 | -------------------------------------------------------------------------------- /tools/theme-download/main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * COPYRIGHT (c) Siemens AG 2018-2024 ALL RIGHTS RESERVED. 3 | */ 4 | import axios from "axios"; 5 | import fs, { ensureDirSync } from "fs-extra"; 6 | import path from "path"; 7 | import zlib from "zlib"; 8 | import * as tar from "tar"; 9 | import { config as dotenv } from "@dotenvx/dotenvx"; 10 | 11 | const __dirname = path.resolve("..", ".."); 12 | 13 | dotenv({ 14 | override: true, 15 | path: path.join(__dirname, ".env"), 16 | }); 17 | 18 | dotenv({ 19 | override: true, 20 | path: path.join(__dirname, ".env.production"), 21 | }); 22 | 23 | const token = process.env.CSC_TOKEN!; 24 | let pkgUrl = process.env.BRAND_URL!; 25 | const pkgVersion = process.env.BRAND_VERSION!; 26 | 27 | if (!pkgUrl) { 28 | console.log("No additional theme configured"); 29 | process.exit(1); 30 | } 31 | 32 | if (!pkgUrl.endsWith(".tgz")) { 33 | pkgUrl = pkgUrl + "-" + pkgVersion + ".tgz"; 34 | } 35 | 36 | if (!process.env.CI) { 37 | console.error("This script should only be run in CI"); 38 | process.exit(1); 39 | } 40 | 41 | if (!token) { 42 | console.error("CSC_TOKEN is required"); 43 | process.exit(1); 44 | } 45 | 46 | const download = async (url: string, file: string) => { 47 | console.log("download"); 48 | const response = await axios.get(url, { 49 | responseType: "arraybuffer", 50 | headers: { 51 | Authorization: `Bearer ${token}`, 52 | }, 53 | }); 54 | 55 | const fileData = Buffer.from(response.data, "binary"); 56 | await fs.writeFile(file, fileData); 57 | }; 58 | 59 | const unpack = async (file: string) => { 60 | const unpackTheme = path.join(file, ".."); 61 | return new Promise((resolve) => 62 | fs 63 | .createReadStream(file) 64 | .pipe(zlib.createGunzip()) 65 | .pipe( 66 | tar.extract({ 67 | cwd: unpackTheme, 68 | }), 69 | ) 70 | .on("finish", () => { 71 | resolve(path.join(unpackTheme, "package")); 72 | }), 73 | ); 74 | }; 75 | 76 | const __node_modules = path.join(__dirname, "node_modules"); 77 | const __cache = path.join(__node_modules, ".cache", "ix-theme-downloader"); 78 | const __themeTgz = path.join(__cache, "theme.tgz"); 79 | 80 | ensureDirSync(__cache); 81 | 82 | if (!fs.existsSync(__node_modules)) { 83 | console.error("node_modules not found"); 84 | process.exit(1); 85 | } 86 | 87 | await download(pkgUrl, __themeTgz); 88 | const unpackTheme = await unpack(__themeTgz); 89 | fs.moveSync(unpackTheme, path.join(__node_modules, "@siemens-ix", "corporate-theme")); 90 | -------------------------------------------------------------------------------- /apps/react-starter/e2e/devices.e2e.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, Page } from "@playwright/test"; 2 | 3 | async function filterDevicePageByDeviceName(page: Page, deviceName: string) { 4 | const categoryFilter = page.locator("ix-category-filter"); 5 | const filterInput = categoryFilter.locator("input"); 6 | 7 | await filterInput.fill("Device name"); 8 | 9 | const dropdown = categoryFilter.locator("ix-dropdown"); 10 | await expect(dropdown.getByText("Categories")).toBeVisible(); 11 | 12 | await page.keyboard.press("Tab"); 13 | await page.keyboard.press("Enter"); 14 | 15 | await expect(categoryFilter.locator("ix-dropdown").getByText("Categories")).not.toBeVisible(); 16 | await expect(categoryFilter.locator("ix-dropdown").getByText("Device name")).toBeVisible(); 17 | 18 | await filterInput.fill(deviceName); 19 | await page.keyboard.press("Enter"); 20 | await page.mouse.click(0, 0); 21 | 22 | await expect(categoryFilter.locator("ix-dropdown")).not.toBeVisible(); 23 | } 24 | 25 | test("filter for a deviceName", async ({ page }) => { 26 | await page.goto("http://localhost:5173/#/devices"); 27 | 28 | const aggrid = page.locator(".ag-root-wrapper"); 29 | const rows = aggrid.locator(".ag-center-cols-container .ag-row"); 30 | 31 | await expect(rows).toHaveCount(22, { 32 | // AG-Grid takes some time to filter the rows 33 | timeout: 500, 34 | }); 35 | 36 | await filterDevicePageByDeviceName(page, "s71200"); 37 | 38 | await expect(rows).toHaveCount(1, { 39 | // AG-Grid takes some time to filter the rows 40 | timeout: 500, 41 | }); 42 | }); 43 | 44 | test("add a new device", async ({ page }) => { 45 | await page.goto("http://localhost:5173/#/devices"); 46 | 47 | const newDeviceName = "My new device"; 48 | 49 | const addDeviceButton = page.getByLabel("add device"); 50 | await addDeviceButton.click(); 51 | 52 | const modal = page.locator("ix-modal"); 53 | 54 | const device = modal.getByLabel("Device Name"); 55 | await device.locator("input").fill(newDeviceName); 56 | 57 | const okayButton = modal.getByLabel("add device"); 58 | await okayButton.click(); 59 | 60 | await expect(modal).not.toBeVisible(); 61 | // Wait for the modal to close and the animation to finish 62 | await expect(modal).not.toBeInViewport(); 63 | 64 | await filterDevicePageByDeviceName(page, newDeviceName); 65 | 66 | const aggrid = page.locator(".ag-root-wrapper"); 67 | const myNewRow = aggrid.getByRole("gridcell", { name: newDeviceName }); 68 | 69 | await expect(myNewRow).toBeVisible(); 70 | }); 71 | -------------------------------------------------------------------------------- /apps/angular-starter/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // import dotenv from 'dotenv'; 8 | // import path from 'path'; 9 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 10 | 11 | /** 12 | * See https://playwright.dev/docs/test-configuration. 13 | */ 14 | export default defineConfig({ 15 | testDir: './e2e', 16 | testMatch: "**/*.e2e.ts", 17 | /* Run tests in files in parallel */ 18 | fullyParallel: true, 19 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 20 | forbidOnly: !!process.env['CI'], 21 | /* Retry on CI only */ 22 | retries: process.env['CI'] ? 2 : 0, 23 | /* Opt out of parallel tests on CI. */ 24 | workers: process.env['CI'] ? 1 : undefined, 25 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 26 | reporter: 'list', 27 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 28 | use: { 29 | /* Base URL to use in actions like `await page.goto('/')`. */ 30 | baseURL: 'http://localhost:4200', 31 | 32 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 33 | trace: 'on-first-retry', 34 | }, 35 | 36 | /* Configure projects for major browsers */ 37 | projects: [ 38 | { 39 | name: 'chromium', 40 | use: { ...devices['Desktop Chrome'] }, 41 | }, 42 | 43 | // { 44 | // name: 'firefox', 45 | // use: { ...devices['Desktop Firefox'] }, 46 | // }, 47 | 48 | // { 49 | // name: 'webkit', 50 | // use: { ...devices['Desktop Safari'] }, 51 | // }, 52 | 53 | /* Test against mobile viewports. */ 54 | // { 55 | // name: 'Mobile Chrome', 56 | // use: { ...devices['Pixel 5'] }, 57 | // }, 58 | // { 59 | // name: 'Mobile Safari', 60 | // use: { ...devices['iPhone 12'] }, 61 | // }, 62 | 63 | /* Test against branded browsers. */ 64 | // { 65 | // name: 'Microsoft Edge', 66 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 67 | // }, 68 | // { 69 | // name: 'Google Chrome', 70 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 71 | // }, 72 | ], 73 | 74 | /* Run your local dev server before starting the tests */ 75 | webServer: { 76 | command: 'pnpm run start', 77 | url: 'http://localhost:4200', 78 | reuseExistingServer: !process.env['CI'], 79 | }, 80 | }); 81 | -------------------------------------------------------------------------------- /apps/react-starter/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // import dotenv from 'dotenv'; 8 | // import path from 'path'; 9 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 10 | 11 | /** 12 | * See https://playwright.dev/docs/test-configuration. 13 | */ 14 | export default defineConfig({ 15 | /* Directory to search for tests */ 16 | testDir: "./e2e", 17 | testMatch: "**/*.e2e.ts", 18 | /* Run tests in files in parallel */ 19 | fullyParallel: true, 20 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 21 | forbidOnly: !!process.env.CI, 22 | /* Retry on CI only */ 23 | retries: process.env.CI ? 2 : 0, 24 | /* Opt out of parallel tests on CI. */ 25 | workers: process.env.CI ? 1 : undefined, 26 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 27 | reporter: "list", 28 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 29 | use: { 30 | /* Base URL to use in actions like `await page.goto('/')`. */ 31 | // baseURL: 'http://127.0.0.1:3000', 32 | 33 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 34 | trace: "on-first-retry", 35 | }, 36 | 37 | /* Configure projects for major browsers */ 38 | projects: [ 39 | { 40 | name: "chromium", 41 | use: { ...devices["Desktop Chrome"] }, 42 | }, 43 | 44 | /* Test against different browsers. */ 45 | // { 46 | // name: "firefox", 47 | // use: { ...devices["Desktop Firefox"] }, 48 | // }, 49 | 50 | // { 51 | // name: "webkit", 52 | // use: { ...devices["Desktop Safari"] }, 53 | // }, 54 | 55 | /* Test against mobile viewports. */ 56 | // { 57 | // name: 'Mobile Chrome', 58 | // use: { ...devices['Pixel 5'] }, 59 | // }, 60 | // { 61 | // name: 'Mobile Safari', 62 | // use: { ...devices['iPhone 12'] }, 63 | // }, 64 | 65 | /* Test against branded browsers. */ 66 | // { 67 | // name: 'Microsoft Edge', 68 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 69 | // }, 70 | // { 71 | // name: 'Google Chrome', 72 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 73 | // }, 74 | ], 75 | 76 | /* Run your local dev server before starting the tests */ 77 | webServer: { 78 | command: "pnpm dev", 79 | url: "http://localhost:5173", 80 | reuseExistingServer: !process.env.CI, 81 | }, 82 | }); 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Siemens Industrial Experience Starter Apps 2 | 3 | [![MIT License](https://img.shields.io/badge/license-MIT-009999.svg?style=flat)](./LICENSE.md) 4 | 5 | iX is an open-source design system for designers and developers, to consistently create the perfect digital experience for industrial software products. 6 | 7 |

8 | Main Repository 9 | - 10 | Quickstart 11 | - 12 | 13 | Documentation 14 | 15 | - 16 | Community 17 |

18 | 19 | 20 | ## What's inside? 21 | 22 | This Turborepo includes the following packages/apps: 23 | 24 | | Name | Description | Deploy | 25 | |---------------|------------------------------------------------------------|--------| 26 | | react-starter | Example application to show some features of the framework | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/siemens/ix-starter/tree/main/apps/react-starter) | 27 | | angular-starter | Example application to show some features of the framework | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/siemens/ix-starter/tree/main/apps/angular-starter) | 28 | 29 | ### Utilities 30 | 31 | This Turborepo has some additional tools already setup for you: 32 | 33 | - [TypeScript](https://www.typescriptlang.org/) for static type checking 34 | - [ESLint](https://eslint.org/) for code linting 35 | - [Prettier](https://prettier.io) for code formatting 36 | 37 | ### Build 38 | 39 | To build all apps and packages, run the following command: 40 | 41 | ``` 42 | pnpm build 43 | ``` 44 | 45 | ### Develop 46 | 47 | To develop all apps and packages, run the following command: 48 | 49 | #### React 50 | 51 | ``` 52 | pnpm dev --filter ix-react-starter 53 | ``` 54 | 55 | 56 | #### Angular 57 | 58 | ``` 59 | pnpm dev --filter ix-angular-starter 60 | 61 | ## Useful Links 62 | 63 | Learn more about the power of Turborepo: 64 | 65 | - [Tasks](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks) 66 | - [Caching](https://turbo.build/repo/docs/core-concepts/caching) 67 | - [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) 68 | - [Filtering](https://turbo.build/repo/docs/core-concepts/monorepos/filtering) 69 | - [Configuration Options](https://turbo.build/repo/docs/reference/configuration) 70 | - [CLI Usage](https://turbo.build/repo/docs/reference/command-line-reference) 71 | -------------------------------------------------------------------------------- /apps/angular-starter/e2e/devices.e2e.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, Page } from "@playwright/test"; 2 | 3 | async function filterDevicePageByDeviceName(page: Page, filterBy: string, filterString: string) { 4 | const categoryFilter = page.locator("ix-category-filter"); 5 | const filterInput = categoryFilter.locator("input"); 6 | 7 | await filterInput.fill(filterBy); 8 | 9 | const dropdown = categoryFilter.locator("ix-dropdown"); 10 | await expect(dropdown.getByText("Categories")).toBeVisible(); 11 | 12 | await page.keyboard.press("Tab"); 13 | await page.keyboard.press("Enter"); 14 | 15 | await expect(categoryFilter.locator("ix-dropdown").getByText("Categories")).not.toBeVisible(); 16 | await expect(categoryFilter.locator("ix-dropdown").getByText(filterBy)).toBeVisible(); 17 | 18 | await filterInput.fill(filterString); 19 | await page.keyboard.press("Enter"); 20 | await page.mouse.click(0, 0); 21 | 22 | await expect(categoryFilter.locator("ix-dropdown")).not.toBeVisible(); 23 | } 24 | 25 | test('should display the devices table', async ({ page }) => { 26 | await page.goto("http://localhost:4200/devices"); 27 | await expect(page.locator('ag-grid-angular')).toBeVisible(); 28 | }); 29 | 30 | 31 | test('should expand a device row on click', async ({ page }) => { 32 | await page.goto("http://localhost:4200/devices"); 33 | const firstRow = page.locator('.ag-center-cols-container .ag-row').first(); 34 | await firstRow.click(); 35 | // Check for expanded details (adjust selector as needed) 36 | await expect(page.locator('ix-pane')).toBeVisible(); 37 | }); 38 | 39 | 40 | test("should filter devices by deviceName", async ({ page }) => { 41 | await page.goto("http://localhost:4200/devices"); 42 | 43 | const aggrid = page.locator(".ag-root-wrapper"); 44 | const rows = aggrid.locator(".ag-center-cols-container .ag-row"); 45 | 46 | await expect(rows).toHaveCount(22, { 47 | // AG-Grid takes some time to filter the rows 48 | timeout: 500, 49 | }); 50 | 51 | await filterDevicePageByDeviceName(page, "Device name", "s71200"); 52 | 53 | await expect(rows).toHaveCount(1, { 54 | // AG-Grid takes some time to filter the rows 55 | timeout: 500, 56 | }); 57 | }); 58 | 59 | test('should filter devices by status', async ({ page }) => { 60 | await page.goto("http://localhost:4200/devices"); 61 | 62 | const aggrid = page.locator(".ag-root-wrapper"); 63 | const rows = aggrid.locator(".ag-center-cols-container .ag-row"); 64 | 65 | await expect(rows).toHaveCount(22, { 66 | // AG-Grid takes some time to filter the rows 67 | timeout: 500, 68 | }); 69 | 70 | await filterDevicePageByDeviceName(page, "Status", "Offline"); 71 | 72 | await expect(rows).toHaveCount(1, { 73 | // AG-Grid takes some time to filter the rows 74 | timeout: 500, 75 | }); 76 | }); 77 | 78 | 79 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/delete-modal/delete-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule, TranslateService } from '@ngx-translate/core'; 3 | import { IxActiveModal } from '@siemens/ix-angular/standalone'; 4 | import { DeleteModalComponent } from './delete-modal.component'; 5 | 6 | describe('DeleteModalComponent', () => { 7 | let component: DeleteModalComponent; 8 | let fixture: ComponentFixture; 9 | let mockActiveModal: jasmine.SpyObj; 10 | 11 | beforeEach(async () => { 12 | const activeModalSpy = jasmine.createSpyObj('IxActiveModal', [ 13 | 'close', 14 | 'dismiss', 15 | ]); 16 | 17 | await TestBed.configureTestingModule({ 18 | imports: [DeleteModalComponent, TranslateModule.forRoot()], 19 | providers: [ 20 | { provide: IxActiveModal, useValue: activeModalSpy }, 21 | TranslateService, 22 | ], 23 | }).compileComponents(); 24 | 25 | fixture = TestBed.createComponent(DeleteModalComponent); 26 | component = fixture.componentInstance; 27 | mockActiveModal = TestBed.inject( 28 | IxActiveModal, 29 | ) as jasmine.SpyObj; 30 | fixture.detectChanges(); 31 | }); 32 | 33 | it('should create', () => { 34 | expect(component).toBeTruthy(); 35 | }); 36 | 37 | it('should close modal with deleted: true when close() is called', () => { 38 | component.close(); 39 | expect(mockActiveModal.close).toHaveBeenCalledWith({ deleted: true }); 40 | }); 41 | 42 | it('should dismiss modal with deleted: false when dismiss() is called', () => { 43 | component.dismiss(); 44 | expect(mockActiveModal.dismiss).toHaveBeenCalledWith({ deleted: false }); 45 | }); 46 | 47 | it('should display translated title', () => { 48 | const compiled = fixture.nativeElement as HTMLElement; 49 | const titleElement = compiled.querySelector('ix-modal-header'); 50 | expect(titleElement).toBeTruthy(); 51 | expect(titleElement?.getAttribute('icon')).toBe('trashcan'); 52 | expect(titleElement?.getAttribute('icon-color')).toBe('color-alarm'); 53 | }); 54 | 55 | it('should display translated content', () => { 56 | const compiled = fixture.nativeElement as HTMLElement; 57 | const contentElement = compiled.querySelector('ix-modal-content'); 58 | expect(contentElement).toBeTruthy(); 59 | }); 60 | 61 | it('should have cancel and delete buttons', () => { 62 | const compiled = fixture.nativeElement as HTMLElement; 63 | const buttons = compiled.querySelectorAll('ix-button'); 64 | expect(buttons.length).toBe(2); 65 | 66 | const cancelButton = buttons[0]; 67 | const deleteButton = buttons[1]; 68 | 69 | expect(cancelButton.getAttribute('variant')).toBe('subtle-secondary'); 70 | expect(deleteButton.getAttribute('variant')).toBe('danger-primary'); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /apps/react-starter/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { registerTheme } from "@siemens/ix-echarts"; 2 | import { iconLogOut, iconUserSettings, iconHome, iconProject } from "@siemens/ix-icons/icons"; 3 | import { 4 | IxApplication, 5 | IxApplicationHeader, 6 | IxAvatar, 7 | IxContent, 8 | IxDropdownItem, 9 | IxMenu, 10 | IxMenuItem, 11 | IxMenuSettings, 12 | IxMenuSettingsItem, 13 | } from "@siemens/ix-react"; 14 | import * as echarts from "echarts/core"; 15 | import { useEffect, useRef } from "react"; 16 | import { useTranslation } from "react-i18next"; 17 | import { NavLink, Outlet } from "react-router-dom"; 18 | import useShowDemoMessage from "./hooks/demoMessage"; 19 | import Logo from "./Logo"; 20 | import UserSettings from "./pages/user-settings"; 21 | import { useDataStore } from "./store/device-store"; 22 | import ApplicationSettings from "./pages/application-settings"; 23 | 24 | registerTheme(echarts); 25 | 26 | function App() { 27 | const menuRef = useRef(null); 28 | const showDemoMessage = useShowDemoMessage(); 29 | const { fetch } = useDataStore(); 30 | 31 | useEffect(() => { 32 | fetch(); 33 | }, [fetch]); 34 | 35 | const { t } = useTranslation(); 36 | 37 | return ( 38 | 39 | 40 | 41 | 42 | 47 | 48 | 49 | 50 | 56 | 57 | {({ isActive }: { isActive: boolean }) => ( 58 | 59 | {t("overview")} 60 | 61 | )} 62 | 63 | 64 | 65 | {({ isActive }: { isActive: boolean }) => ( 66 | 67 | {t("devices")} 68 | 69 | )} 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | ); 87 | } 88 | 89 | export default App; 90 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: ["main"] # remove "react-demo-app" before merge to main 7 | pull_request: 8 | types: 9 | - opened 10 | - reopened 11 | - synchronize 12 | 13 | concurrency: 14 | group: pr-${{ github.head_ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | build: 19 | name: Build 20 | timeout-minutes: 15 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | - uses: ./.github/workflows/actions/install 25 | 26 | - name: Build 27 | run: pnpm build 28 | 29 | lint: 30 | name: Lint 31 | timeout-minutes: 15 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - uses: actions/checkout@v3 36 | - uses: ./.github/workflows/actions/install 37 | 38 | - name: Lint 39 | run: pnpm lint 40 | 41 | unit: 42 | name: Unit 43 | timeout-minutes: 15 44 | runs-on: ubuntu-latest 45 | 46 | steps: 47 | - uses: actions/checkout@v3 48 | - uses: ./.github/workflows/actions/install 49 | - name: Install Playwright Browsers 50 | run: | 51 | cd ./apps/react-starter 52 | pnpm exec playwright install chromium --with-deps 53 | 54 | - name: Test (React) 55 | run: pnpm --filter ix-react-starter test 56 | 57 | - name: Test (Angular) 58 | run: pnpm --filter ix-angular-starter test 59 | 60 | e2e: 61 | # Depend on build job to ensure the build is completed before running E2E tests 62 | # needs: [build] 63 | name: E2E 64 | timeout-minutes: 15 65 | runs-on: ubuntu-latest 66 | 67 | steps: 68 | - uses: actions/checkout@v3 69 | - uses: ./.github/workflows/actions/install 70 | 71 | # React E2E 72 | - name: Install Playwright Browsers (React) 73 | run: | 74 | cd ./apps/react-starter 75 | pnpm playwright install chromium --with-deps 76 | 77 | - name: E2E (React) 78 | run: pnpm --filter ix-react-starter e2e 79 | 80 | - name: E2E Report (React) 81 | if: ${{ !cancelled() }} 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: react-playwright-report 85 | path: apps/react-starter/playwright-report/ 86 | retention-days: 1 87 | 88 | # Angular E2E 89 | - name: Install Playwright Browsers (Angular) 90 | run: | 91 | cd ./apps/angular-starter 92 | pnpm playwright install chromium --with-deps 93 | 94 | - name: E2E (Angular) 95 | run: pnpm --filter ix-angular-starter e2e 96 | 97 | - name: E2E Report (Angular) 98 | if: ${{ !cancelled() }} 99 | uses: actions/upload-artifact@v4 100 | with: 101 | name: angular-playwright-report 102 | path: apps/angular-starter/playwright-report/ 103 | retention-days: 1 104 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/action-cell-renderer/action-cell-renderer.component.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | {{ "dropdown-quick-actions.rename" | translate }} 10 | 11 | 17 | 18 | 19 | {{ "dropdown-quick-actions.delete" | translate }} 20 | 21 | 22 | 28 | 29 | 30 | 38 | 39 | 45 | Duplicate 46 | 52 | Cut 53 | 59 | Copy 60 | 67 | Paste 68 | 69 | 70 | 75 | 85 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /apps/react-starter/src/store/device-store.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import { create } from "zustand"; 11 | import { fetchDataSheet } from "../util/mock-api.ts"; 12 | import { Device } from "../types/index.tsx"; 13 | import { LogicalFilterOperator } from "@siemens/ix"; 14 | 15 | interface DataStoreState { 16 | devices: Device[]; 17 | addDevice: (device: Device) => void; 18 | pasteDevice: (device: Device, position: number) => void; 19 | deleteDevice: (device: Device) => void; 20 | editDevice: (device: Device) => void; 21 | fetch: () => Promise; 22 | } 23 | 24 | export const useDataStore = create((set) => ({ 25 | devices: [], 26 | addDevice: (device: Device) => 27 | set((state) => { 28 | const newDevice = { ...device, id: state.devices.length.toString() }; 29 | return { devices: [...state.devices, newDevice] }; 30 | }), 31 | pasteDevice: (device: Device, position: number) => 32 | set((state) => { 33 | const newDevice = { ...device, id: position.toString() }; 34 | const updatedDevices = [ 35 | ...state.devices.slice(0, position), 36 | newDevice, 37 | ...state.devices.slice(position), 38 | ]; 39 | return { devices: updatedDevices }; 40 | }), 41 | deleteDevice: (device: Device) => 42 | set((state) => ({ devices: state.devices.filter((d) => d.id !== device.id) })), 43 | editDevice: (device: Device) => 44 | set((state) => ({ devices: state.devices.map((d) => (d.id === device.id ? device : d)) })), 45 | fetch: async () => { 46 | const response = await fetchDataSheet(); 47 | const devicesWithId = response.map((device, index) => ({ 48 | ...device, 49 | id: (index + 1).toString(), 50 | })); 51 | set({ devices: devicesWithId }); 52 | }, 53 | })); 54 | 55 | interface OverviewPaneStore { 56 | expanded: boolean; 57 | selectedDataId: string | null; 58 | setExpanded: (expanded: boolean) => void; 59 | setSelectedDeviceId: (selectedDataId: string) => void; 60 | } 61 | 62 | export const useOverviewPaneStore = create((set) => ({ 63 | expanded: false, 64 | selectedDataId: null, 65 | setExpanded: (expanded: boolean) => set({ expanded }), 66 | setSelectedDeviceId: (selectedDataId: string) => set({ selectedDataId }), 67 | })); 68 | 69 | export interface Filter { 70 | id: string; 71 | value: string; 72 | operator: LogicalFilterOperator; 73 | } 74 | 75 | interface FilterStore { 76 | filter: Filter[]; 77 | setFilter: (filter: Filter[]) => void; 78 | resetFilter: () => void; 79 | } 80 | 81 | export const useFilterStore = create((set) => ({ 82 | filter: [], 83 | setFilter: (filter: Filter[]) => { 84 | set((state) => { 85 | if (filter.length !== state.filter.length) { 86 | return { filter }; 87 | } 88 | 89 | return state; 90 | }); 91 | }, 92 | resetFilter: () => set({ filter: [] }), 93 | })); 94 | -------------------------------------------------------------------------------- /apps/react-starter/src/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "analytics": "Analytics", 3 | "cards": "Cards", 4 | "content-header": "Hello iX user, welcome to our demo app!", 5 | "device-status": { 6 | "title": "Device status" 7 | }, 8 | "devices": "Devices", 9 | "add-device": "Add device", 10 | "device-quick-actions": { 11 | "add-device": "Add device", 12 | "devices": "Devices", 13 | "faulty-devices": "Errors", 14 | "warning-devices": "Servicing" 15 | }, 16 | "category-filter": { 17 | "placeholder": "Filter by", 18 | "categories": "Categories" 19 | }, 20 | "device-add-modal": { 21 | "title": "Add device", 22 | "list-header": "Please select a status", 23 | "close": "Add device", 24 | "dismiss": "Cancel", 25 | "success": "Device successfully added!" 26 | }, 27 | "device-delete-modal": { 28 | "title": "Delete device?", 29 | "content": "Do you really want to delete the device?", 30 | "close": "Delete device", 31 | "dismiss": "Cancel" 32 | }, 33 | "dropdown-quick-actions": { 34 | "rename": "Rename", 35 | "on": "Turn on", 36 | "off": "Turn off", 37 | "delete": "Delete", 38 | "success-messages": { 39 | "copy": "Device copied to clipboard", 40 | "delete": "Device deleted", 41 | "duplicate": "Device duplicated", 42 | "cut": "Device cut to clipboard", 43 | "paste": "Device pasted" 44 | } 45 | }, 46 | "device-details-header": { 47 | "title": "Device details", 48 | "firmware-card": "Firmware is not up to date", 49 | "update": "Update" 50 | }, 51 | "device-details-footer": { 52 | "set-maintenance": "Start maintenance", 53 | "end-maintenance": "End maintenance" 54 | }, 55 | "device-details": { 56 | "title": "Device information", 57 | "device-name": "Device name", 58 | "vendor": "Vendor", 59 | "description": "Device type", 60 | "status": "Status", 61 | "article-number": "Article number", 62 | "mac-address": "MAC address", 63 | "ip-address": "IP address", 64 | "firmware-version": "Firmware version", 65 | "serial-number": "Serial number" 66 | }, 67 | "incidents": { 68 | "title": "Incidents", 69 | "create-task": "Create task", 70 | "incident-cards": { 71 | "device": "Device", 72 | "date": "Date" 73 | } 74 | }, 75 | "language": { 76 | "title": "Language", 77 | "de": "German", 78 | "en": "English" 79 | }, 80 | "list": "List", 81 | "overview": "Overview", 82 | "quick-actions": { 83 | "title": "Quick Actions", 84 | "add-device": "Add device", 85 | "add-devices": "Add devices", 86 | "schedule-maintenance": "Schedule maintenance" 87 | }, 88 | "status-history": { 89 | "title": "Status history" 90 | }, 91 | "settings": { 92 | "title": "Settings", 93 | "user-settings": "User settings", 94 | "application-settings": "Application settings", 95 | "notification": "Notifications", 96 | "toggle-on": "On", 97 | "toggle-off": "Off", 98 | "import-device": "Import device data", 99 | "upload-text": "Drag files here or...", 100 | "upload-button": "Upload files..." 101 | }, 102 | "toggle-theme": "Toggle theme", 103 | "search": "Search", 104 | "demo-message": "This feature is currently unavailable in the demo version.", 105 | "cancel": "Cancel", 106 | "okay": "OK" 107 | } 108 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject, OnInit } from '@angular/core'; 2 | import { 3 | NavigationEnd, 4 | Router, 5 | RouterLink, 6 | RouterOutlet, 7 | } from '@angular/router'; 8 | import { 9 | IxApplication, 10 | IxApplicationHeader, 11 | IxAvatar, 12 | IxDropdownItem, 13 | IxMenu, 14 | IxMenuItem, 15 | IxMenuSettingsItem, 16 | IxMenuSettings, 17 | IxTypography, 18 | IxToggle, 19 | IxUpload, 20 | } from '@siemens/ix-angular/standalone'; 21 | import { addIcons } from '@siemens/ix-icons'; 22 | import { 23 | iconStar, 24 | iconStarFilled, 25 | iconHome, 26 | iconProject, 27 | iconUserSettings, 28 | iconLogOut, 29 | } from '@siemens/ix-icons/icons'; 30 | import { useShowDemoMessage } from './shared/utlis'; 31 | import { SettingsComponent } from './pages/settings/settings/settings.component'; 32 | import { TranslateModule, TranslateService } from '@ngx-translate/core'; 33 | import { filter } from 'rxjs'; 34 | import { themeSwitcher } from '@siemens/ix'; 35 | 36 | @Component({ 37 | selector: 'app-root', 38 | standalone: true, 39 | imports: [ 40 | IxApplication, 41 | IxApplicationHeader, 42 | IxMenu, 43 | IxMenuItem, 44 | IxAvatar, 45 | IxDropdownItem, 46 | IxMenuSettingsItem, 47 | IxMenuSettings, 48 | RouterOutlet, 49 | RouterLink, 50 | IxTypography, 51 | IxToggle, 52 | IxUpload, 53 | SettingsComponent, 54 | TranslateModule, 55 | ], 56 | templateUrl: './app.component.html', 57 | 58 | styleUrl: './app.component.scss', 59 | }) 60 | export class AppComponent implements OnInit { 61 | activePage = 'overview'; 62 | activeSettingsTab = 'user-settings'; // Track active settings tab 63 | private readonly router = inject(Router); 64 | private readonly translateService = inject(TranslateService); 65 | constructor() { 66 | addIcons({ 67 | iconStar, 68 | iconStarFilled, 69 | iconHome, 70 | iconProject, 71 | iconUserSettings, 72 | iconLogOut, 73 | }); 74 | } 75 | 76 | ngOnInit() { 77 | themeSwitcher.setTheme('theme-classic-dark'); 78 | this.setActivePageFromUrl(this.router.url); 79 | // Listen to route changes 80 | this.router.events 81 | .pipe(filter((event) => event instanceof NavigationEnd)) 82 | .subscribe((event: NavigationEnd) => { 83 | this.setActivePageFromUrl(event.url); 84 | }); 85 | 86 | // Ensure the first tab is active by default 87 | this.activeSettingsTab = 'user-settings'; 88 | } 89 | 90 | private setActivePageFromUrl(url: string) { 91 | if (url.includes('devices')) { 92 | this.activePage = 'devices'; 93 | } else if (url.includes('overview')) { 94 | this.activePage = 'overview'; 95 | } 96 | } 97 | 98 | openModal() { 99 | useShowDemoMessage(); 100 | } 101 | 102 | setActiveSettingsTab(event: any) { 103 | const selectedLabel = event.detail; 104 | 105 | const userSettingsLabel = this.translateService.instant('settings.user-settings'); 106 | const appSettingsLabel = this.translateService.instant('settings.application-settings'); 107 | 108 | if (selectedLabel === userSettingsLabel) { 109 | this.activeSettingsTab = 'user-settings'; 110 | } else if (selectedLabel === appSettingsLabel) { 111 | this.activeSettingsTab = 'application-settings'; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /apps/angular-starter/src/assets/mock-data/Incidents.ts: -------------------------------------------------------------------------------- 1 | export const INCIDENTS = [ 2 | { 3 | id: 1, 4 | incidentName: 'Update available', 5 | icon: 'cloud-upload', 6 | infoText: 'v2.3 -> v2.5', 7 | deviceName: 'robo1-net-sw17', 8 | ipAddress: '172.19.65.8', 9 | date: '2024-10-09', 10 | time: '08:51:21', 11 | color: 'info', 12 | }, 13 | { 14 | id: 2, 15 | incidentName: 'Maintenance ended', 16 | icon: 'maintenance-warning', 17 | infoText: 'Manually ended by user', 18 | deviceName: 'agv-net-ap06', 19 | ipAddress: '172.19.62.12', 20 | date: '2024-10-08', 21 | time: '15:20:07', 22 | color: 'warning', 23 | }, 24 | { 25 | id: 3, 26 | incidentName: 'Maintenance started', 27 | icon: 'maintenance-warning', 28 | infoText: 'Manually started by user', 29 | deviceName: 'agv-net-ap06', 30 | ipAddress: '172.19.62.12', 31 | date: '2024-10-08', 32 | time: '14:55:12', 33 | color: 'warning', 34 | }, 35 | { 36 | id: 4, 37 | incidentName: 'Vendor changed', 38 | icon: 'info', 39 | infoText: 'SIEMENS AG -> Siemens', 40 | deviceName: '01320ht01.x2p1', 41 | ipAddress: '10.160.16.6', 42 | date: '2024-10-08', 43 | time: '13:10:50', 44 | color: 'neutral', 45 | }, 46 | { 47 | id: 5, 48 | incidentName: 'Device name changed', 49 | icon: 'info', 50 | infoText: 'NYC-Cisco-ASR9001 -> NYC-Cisco-ASR9001-Edge-Router', 51 | deviceName: 'NYC-Cisco-ASR9001-Edge-Router', 52 | ipAddress: '192.168.17.51', 53 | date: '2024-10-08', 54 | time: '08:01:44', 55 | color: 'neutral', 56 | }, 57 | { 58 | id: 6, 59 | incidentName: 'Maintenance started', 60 | icon: 'maintenance-warning', 61 | infoText: 'Status update from device', 62 | deviceName: 'pd-rack-pnet200sp-01', 63 | ipAddress: '172.27.232.65', 64 | date: '2024-10-07', 65 | time: '23:20:27', 66 | color: 'warning', 67 | }, 68 | { 69 | id: 7, 70 | incidentName: 'Device type changed', 71 | icon: 'info', 72 | infoText: 'Level radar -> Sensor', 73 | deviceName: 'tanklevel-sensor', 74 | ipAddress: '192.168.158.178', 75 | date: '2024-10-07', 76 | time: '17:00:04', 77 | color: 'neutral', 78 | }, 79 | { 80 | id: 8, 81 | incidentName: 'Maintenance started', 82 | icon: 'maintenance-warning', 83 | infoText: 'Status update from device', 84 | deviceName: 'pd-rackkm-cpu1712-08', 85 | ipAddress: '172.27.232.95', 86 | date: '2024-10-06', 87 | time: '18:59:10', 88 | color: 'warning', 89 | }, 90 | { 91 | id: 9, 92 | incidentName: 'Error', 93 | icon: 'error', 94 | infoText: 'Status update from device', 95 | deviceName: 'heatexchanger-sensor', 96 | ipAddress: '192.168.145.189', 97 | date: '2024-10-06', 98 | time: '14:07:45', 99 | color: 'alarm', 100 | }, 101 | { 102 | id: 10, 103 | incidentName: 'Device offline', 104 | icon: 'info', 105 | infoText: 'Status update from device', 106 | deviceName: 's71200', 107 | ipAddress: '192.168.74.1', 108 | date: '2024-10-06', 109 | time: '14:06:12', 110 | color: 'neutral', 111 | }, 112 | ]; -------------------------------------------------------------------------------- /apps/angular-starter/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ixd-angular-starter": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "style": "scss" 11 | } 12 | }, 13 | "root": "", 14 | "sourceRoot": "src", 15 | "prefix": "app", 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:application", 19 | "options": { 20 | "outputPath": "dist", 21 | "index": "src/index.html", 22 | "browser": "src/main.ts", 23 | "polyfills": ["zone.js"], 24 | "tsConfig": "tsconfig.app.json", 25 | "inlineStyleLanguage": "scss", 26 | "assets": [ 27 | { 28 | "glob": "**/*", 29 | "input": "public" 30 | }, 31 | "src/assets" 32 | ], 33 | "styles": ["src/styles.scss"], 34 | "scripts": [] 35 | }, 36 | "configurations": { 37 | "production": { 38 | "budgets": [ 39 | { 40 | "type": "initial", 41 | "maximumWarning": "5MB", 42 | "maximumError": "5MB" 43 | }, 44 | { 45 | "type": "anyComponentStyle", 46 | "maximumWarning": "4kB", 47 | "maximumError": "8kB" 48 | } 49 | ], 50 | "outputHashing": "all" 51 | }, 52 | "development": { 53 | "optimization": false, 54 | "extractLicenses": false, 55 | "sourceMap": true 56 | } 57 | }, 58 | "defaultConfiguration": "production" 59 | }, 60 | "serve": { 61 | "builder": "@angular-devkit/build-angular:dev-server", 62 | "configurations": { 63 | "production": { 64 | "buildTarget": "ixd-angular-starter:build:production" 65 | }, 66 | "development": { 67 | "buildTarget": "ixd-angular-starter:build:development" 68 | } 69 | }, 70 | "defaultConfiguration": "development" 71 | }, 72 | "extract-i18n": { 73 | "builder": "@angular-devkit/build-angular:extract-i18n" 74 | }, 75 | "test": { 76 | "builder": "@angular-devkit/build-angular:karma", 77 | "options": { 78 | "polyfills": ["zone.js", "zone.js/testing"], 79 | "tsConfig": "tsconfig.spec.json", 80 | "karmaConfig": "karma.config.js", 81 | "inlineStyleLanguage": "scss", 82 | "assets": [ 83 | { 84 | "glob": "**/*", 85 | "input": "public" 86 | } 87 | ], 88 | "styles": ["src/styles.scss"], 89 | "scripts": [] 90 | } 91 | }, 92 | "lint": { 93 | "builder": "@angular-eslint/builder:lint", 94 | "options": { 95 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] 96 | } 97 | } 98 | } 99 | } 100 | }, 101 | "cli": { 102 | "schematicCollections": [ 103 | "angular-eslint" 104 | ], 105 | "analytics": false 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/devices.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { TranslateModule } from '@ngx-translate/core'; 3 | import { DevicesComponent } from './devices.component'; 4 | import { ModuleRegistry, AllCommunityModule } from 'ag-grid-community'; 5 | 6 | describe('DevicesComponent', () => { 7 | let component: DevicesComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async () => { 11 | ModuleRegistry.registerModules([AllCommunityModule]); 12 | await TestBed.configureTestingModule({ 13 | imports: [DevicesComponent, TranslateModule.forRoot()], 14 | }).compileComponents(); 15 | 16 | fixture = TestBed.createComponent(DevicesComponent); 17 | component = fixture.componentInstance; 18 | fixture.detectChanges(); 19 | }); 20 | 21 | it('should create', () => { 22 | expect(component).toBeTruthy(); 23 | }); 24 | }); 25 | describe('categories', () => { 26 | let component: DevicesComponent; 27 | let fixture: ComponentFixture; 28 | 29 | beforeEach(async () => { 30 | await TestBed.configureTestingModule({ 31 | imports: [DevicesComponent, TranslateModule.forRoot()], 32 | }).compileComponents(); 33 | 34 | fixture = TestBed.createComponent(DevicesComponent); 35 | component = fixture.componentInstance; 36 | fixture.detectChanges(); 37 | }); 38 | 39 | it('should have deviceName options matching unique device names in rowData', () => { 40 | const uniqueDeviceNames = [ 41 | ...new Set(component.rowData.map((item: any) => item.deviceName)), 42 | ]; 43 | expect(component.categories.deviceName.options).toEqual(uniqueDeviceNames); 44 | }); 45 | 46 | it('should have vendor options matching unique vendors in rowData', () => { 47 | const uniqueVendors = [ 48 | ...new Set(component.rowData.map((item: any) => item.vendor)), 49 | ]; 50 | expect(component.categories.vendor.options).toEqual(uniqueVendors); 51 | }); 52 | 53 | it('should have deviceType options matching unique device types in rowData', () => { 54 | const uniqueDeviceTypes = [ 55 | ...new Set(component.rowData.map((item: any) => item.deviceType)), 56 | ]; 57 | expect(component.categories.deviceType.options).toEqual(uniqueDeviceTypes); 58 | }); 59 | 60 | it('should have status options matching unique statuses in rowData', () => { 61 | const uniqueStatuses = [ 62 | ...new Set(component.rowData.map((item: any) => item.status)), 63 | ]; 64 | expect(component.categories.status.options).toEqual(uniqueStatuses); 65 | }); 66 | 67 | it('should have IPAddress options matching unique IP addresses in rowData', () => { 68 | const uniqueIPAddresses = [ 69 | ...new Set(component.rowData.map((item: any) => item.IPAddress)), 70 | ]; 71 | expect(component.categories.IPAddress.options).toEqual(uniqueIPAddresses); 72 | }); 73 | 74 | it('should have correct labels for each category', () => { 75 | // In test environment, translation keys are returned instead of translated values 76 | expect(component.categories.deviceName.label).toBe( 77 | 'device-details.device-name', 78 | ); 79 | expect(component.categories.vendor.label).toBe('device-details.vendor'); 80 | expect(component.categories.deviceType.label).toBe( 81 | 'device-details.device-type', 82 | ); 83 | expect(component.categories.status.label).toBe('device-details.status'); 84 | expect(component.categories.IPAddress.label).toBe( 85 | 'device-details.ipaddress', 86 | ); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /apps/react-starter/src/locales/de/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "analytics": "Analyse", 3 | "cards": "Karten", 4 | "content-header": "Hallo iX User, willkommen in unserer Demo-App!", 5 | "device-status": { 6 | "title": "Gerätestatus" 7 | }, 8 | "devices": "Geräte", 9 | "add-device": "Gerät hinzufügen", 10 | "device-quick-actions": { 11 | "add-device": "Gerät hinzufügen", 12 | "devices": "Geräte", 13 | "faulty-devices": "Fehler", 14 | "warning-devices": "Wartung" 15 | }, 16 | "category-filter": { 17 | "placeholder": "Filtern nach", 18 | "categories": "Kategorien" 19 | }, 20 | "device-add-modal": { 21 | "title": "Gerät hinzufügen", 22 | "list-header": "Bitte wählen sie einen Status", 23 | "close": "Gerät hinzufügen", 24 | "dismiss": "Abbrechen", 25 | "success": "Device erfolgreich hinzugefügt!" 26 | }, 27 | "device-delete-modal": { 28 | "title": "Gerät löschen?", 29 | "content": "Wollen Sie das Gerät wirklich löschen?", 30 | "close": "Gerät löschen", 31 | "dismiss": "Abbrechen" 32 | }, 33 | "dropdown-quick-actions": { 34 | "rename": "Umbennen", 35 | "on": "Einschalten", 36 | "off": "Ausschalten", 37 | "delete": "Löschen", 38 | "success-messages": { 39 | "copy": "Gerät erfolgreich in die Zwischenablage kopiert!", 40 | "delete": "Gerät erfolgreich entfernt!", 41 | "duplicate": "Gerät erfolgreich dupliziert!", 42 | "cut": "Gerät erfolgreich in die Zwischenablage übertragen!", 43 | "paste": "Gerät erfolgreich eingefügt!" 44 | } 45 | }, 46 | "device-details-header": { 47 | "title": "Geräteinformationen", 48 | "firmware-card": "Gerätefirmware ist veraltet.", 49 | "update": "Update" 50 | }, 51 | "device-details-footer": { 52 | "set-maintenance": "Wartung starten", 53 | "end-maintenance": "Wartung beenden" 54 | }, 55 | "device-details": { 56 | "device-name": "Gerätename", 57 | "vendor": "Hersteller", 58 | "description": "Gerätetyp", 59 | "status": "Status", 60 | "article-number": "Artikelnummer", 61 | "mac-address": "MAC Adresse", 62 | "ip-address": "IP Adresse", 63 | "firmware-version": "Firmware-Version", 64 | "serial-number": "Serialnummer" 65 | }, 66 | "incidents": { 67 | "title": "Zwischenfälle", 68 | "create-task": "Aufgabe erstellen", 69 | "incident-cards": { 70 | "device": "Gerät", 71 | "date": "Datum" 72 | } 73 | }, 74 | "language": { 75 | "title": "Sprache", 76 | "de": "Deutsch", 77 | "en": "Englisch" 78 | }, 79 | "list": "Liste", 80 | "overview": "Übersicht", 81 | "quick-actions": { 82 | "title": "Quick Actions", 83 | "add-device": "Gerät hinzufügen", 84 | "add-devices": "Geräte hinzufügen", 85 | "schedule-maintenance": "Warung planen" 86 | }, 87 | "status-history": { 88 | "title": "Statusverlauf" 89 | }, 90 | "settings": { 91 | "title": "Einstellungen", 92 | "user-settings": "Benutzereinstellungen", 93 | "application-settings": "Anwendungseinstellungen", 94 | "notification": "Benachrichtigungen", 95 | "toggle-on": "Eingeschaltet", 96 | "toggle-off": "Ausgeschaltet", 97 | "import-device": "Gerätedaten importieren", 98 | "upload-text": "Dateien hierher ziehen oder...", 99 | "upload-button": "Dateien auswählen..." 100 | }, 101 | "toggle-theme": "Theme wechseln", 102 | "search": "Suchen", 103 | "demo-message": "Diese Funktion ist in der Demo-Version derzeit nicht verfügbar.", 104 | "cancel": "Abbrechen", 105 | "okay": "OK" 106 | } 107 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/components/add-device-model/add-device-model.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ "device-add-modal.title" | translate }} 3 | 4 | 5 | 6 |
7 |
8 | 13 | 14 | 19 | 20 |
21 | 27 | 28 |
29 | 35 | 36 | 37 | 41 | 42 | 43 | 44 | 49 | 50 | 51 | 56 | 57 | 58 | 63 | 64 | 65 | 70 | 71 | 76 | 77 |
78 |
79 |
80 | 81 | 87 | {{ "device-add-modal.dismiss" | translate }} 88 | 89 | 96 | {{ "device-add-modal.close" | translate }} 97 | 98 | 99 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/status-history/status-history.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild, OnDestroy } from '@angular/core'; 2 | import { 3 | IxCard, 4 | IxCardContent, 5 | IxTypography, 6 | } from '@siemens/ix-angular/standalone'; 7 | import { NgxEchartsDirective, provideEchartsCore } from 'ngx-echarts'; 8 | import { getComputedCSSProperty } from '@siemens/ix-echarts'; 9 | import * as echarts from 'echarts/core'; 10 | import { EChartsOption } from 'echarts'; 11 | import { LineChart } from 'echarts/charts'; 12 | import { TranslateModule } from '@ngx-translate/core'; 13 | import { BehaviorSubject } from 'rxjs'; 14 | import { themeSwitcher, Disposable } from '@siemens/ix'; 15 | import { AsyncPipe } from '@angular/common'; 16 | 17 | echarts.use([LineChart]); 18 | 19 | @Component({ 20 | selector: 'app-status-history', 21 | imports: [ 22 | IxCard, 23 | IxCardContent, 24 | IxTypography, 25 | NgxEchartsDirective, 26 | TranslateModule, 27 | AsyncPipe, 28 | ], 29 | templateUrl: './status-history.component.html', 30 | styleUrl: './status-history.component.scss', 31 | providers: [provideEchartsCore({ echarts })], 32 | }) 33 | export class StatusHistoryComponent implements OnInit, OnDestroy { 34 | @ViewChild(NgxEchartsDirective) chart!: NgxEchartsDirective; 35 | 36 | themeSubject = new BehaviorSubject('theme-classic-dark'); 37 | theme$ = this.themeSubject.asObservable(); 38 | themeSwitchDisposable?: Disposable; 39 | 40 | seriesOnline = { 41 | name: 'Online', 42 | color: getComputedCSSProperty('color-success'), 43 | data: [60, 75, 100, 60, 75, 60], 44 | }; 45 | 46 | seriesOffline = { 47 | name: 'Offline', 48 | color: getComputedCSSProperty('color-neutral'), 49 | data: [-30, -62, -25, -61, -99, -60], 50 | }; 51 | 52 | seriesErrors = { 53 | name: 'Errors', 54 | color: getComputedCSSProperty('color-alarm'), 55 | data: [0, 17, -39, -60, -20, -2], 56 | }; 57 | 58 | seriesMaintenance = { 59 | name: 'Maintenance', 60 | color: getComputedCSSProperty('color-warning'), 61 | data: [0, 2, -90, -85, -3, -1], 62 | }; 63 | 64 | options: EChartsOption = { 65 | legend: { 66 | show: true, 67 | bottom: '0', 68 | left: '0', 69 | icon: 'rect', 70 | }, 71 | 72 | grid: { 73 | top: 10, 74 | bottom: 85, 75 | left: 40, 76 | right: 10, 77 | }, 78 | 79 | xAxis: { 80 | type: 'category', 81 | data: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], 82 | boundaryGap: false, 83 | splitLine: { 84 | show: true, 85 | }, 86 | }, 87 | yAxis: { 88 | type: 'value', 89 | splitLine: { 90 | show: true, 91 | }, 92 | }, 93 | series: [ 94 | { 95 | type: 'line', 96 | ...this.seriesOnline, 97 | }, 98 | { 99 | type: 'line', 100 | ...this.seriesMaintenance, 101 | }, 102 | { 103 | type: 'line', 104 | ...this.seriesErrors, 105 | }, 106 | { 107 | type: 'line', 108 | ...this.seriesOffline, 109 | }, 110 | ], 111 | }; 112 | 113 | ngOnInit(): void { 114 | const currentTheme = themeSwitcher.getCurrentTheme(); 115 | this.themeSubject.next(currentTheme); 116 | this.themeSwitchDisposable = themeSwitcher.themeChanged.on( 117 | (theme: string) => { 118 | this.themeSubject.next(theme); 119 | }, 120 | ); 121 | } 122 | 123 | ngOnDestroy(): void { 124 | this.themeSwitchDisposable?.dispose(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/overview/components/status-history/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import styles from "./styles.module.css"; 11 | 12 | import { IxCard, IxCardContent, IxTypography } from "@siemens/ix-react"; 13 | import { getComputedCSSProperty } from "@siemens/ix-echarts"; 14 | import ReactEcharts from "echarts-for-react"; 15 | import { useTranslation } from "react-i18next"; 16 | import { useRef, useState } from "react"; 17 | import { useResizeHandler } from "../../../../util/util.ts"; 18 | import EChartsReact from "echarts-for-react"; 19 | import { useEChartsTheme } from "@/hooks/theme.ts"; 20 | import { ECBasicOption } from "echarts/types/dist/shared"; 21 | 22 | const seriesOnline = { 23 | name: "Online", 24 | color: [getComputedCSSProperty("color-success")], 25 | data: [{ value: 60 }, { value: 75 }, { value: 100 }, { value: 60 }, { value: 75 }, { value: 60 }], 26 | }; 27 | 28 | const seriesOffline = { 29 | name: "Offline", 30 | color: [getComputedCSSProperty("color-neutral")], 31 | data: [ 32 | { value: -30 }, 33 | { value: -62 }, 34 | { value: -25 }, 35 | { value: -61 }, 36 | { value: -99 }, 37 | { value: -60 }, 38 | ], 39 | }; 40 | 41 | const seriesErrors = { 42 | name: "Errors", 43 | color: getComputedCSSProperty("color-alarm"), 44 | data: [ 45 | { value: 0 }, 46 | { value: 17 }, 47 | { value: -39 }, 48 | { value: -60 }, 49 | { value: -20 }, 50 | { value: -2 }, 51 | ], 52 | }; 53 | 54 | const seriesMaintenance = { 55 | name: "Maintenance", 56 | color: getComputedCSSProperty("color-warning"), 57 | data: [{ value: 0 }, { value: 2 }, { value: -90 }, { value: -85 }, { value: -3 }, { value: -1 }], 58 | }; 59 | 60 | function getOption(): ECBasicOption { 61 | return { 62 | grid: { 63 | top: 10, 64 | bottom: 85, 65 | left: 40, 66 | right: 10, 67 | }, 68 | legend: { 69 | orient: "horizontal", 70 | icon: "rect", 71 | left: "1", 72 | bottom: -0, 73 | }, 74 | xAxis: { 75 | data: ["Jan", "Feb", "Mar", "Apr", "May", "Jun"], 76 | boundaryGap: false, // Ensure the first label starts at the beginning 77 | splitLine: { 78 | show: true, 79 | }, 80 | }, 81 | yAxis: { 82 | splitLine: { 83 | show: true, 84 | }, 85 | }, 86 | series: [ 87 | { 88 | type: "line", 89 | ...seriesOnline, 90 | }, 91 | { 92 | type: "line", 93 | ...seriesMaintenance, 94 | }, 95 | { 96 | type: "line", 97 | ...seriesErrors, 98 | }, 99 | { 100 | type: "line", 101 | ...seriesOffline, 102 | }, 103 | ], 104 | }; 105 | } 106 | 107 | function StatusHistory() { 108 | const { t } = useTranslation(); 109 | const [option] = useState(getOption()); 110 | const chartRef = useRef(null); 111 | const theme = useEChartsTheme(); 112 | useResizeHandler(chartRef); 113 | 114 | return ( 115 | 116 | 117 | 118 | {t("status-history.title")} 119 | 120 | setTimeout(echarts.resize)} 123 | className={styles.echarts} 124 | option={option} 125 | theme={theme} 126 | /> 127 | 128 | 129 | ); 130 | } 131 | 132 | export default StatusHistory; 133 | -------------------------------------------------------------------------------- /apps/react-starter/src/Logo.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | const MyCompany = () => { 11 | return ( 12 | 13 | 19 | 20 | ); 21 | }; 22 | 23 | const Logo = () => { 24 | return ( 25 |
26 | 27 |
28 | ); 29 | }; 30 | 31 | export default Logo; 32 | -------------------------------------------------------------------------------- /apps/angular-starter/src/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "analytics": "Analytics", 3 | "cards": "Cards", 4 | "content-header": "Hello iX user, welcome to our demo app!", 5 | "device-status": { 6 | "title": "Device status" 7 | }, 8 | "devices": "Devices", 9 | "add-device": "Add device", 10 | "device-quick-actions": { 11 | "add-device": "Add device", 12 | "devices": "Devices", 13 | "faulty-devices": "Errors", 14 | "warning-devices": "Servicing" 15 | }, 16 | "category-filter": { 17 | "placeholder": "Filter by", 18 | "categories": "Categories" 19 | }, 20 | "device-add-modal": { 21 | "title": "Add device", 22 | "list-header": "Please select a status", 23 | "close": "Add device", 24 | "dismiss": "Cancel", 25 | "success": "Device successfully added!" 26 | }, 27 | "device-delete-modal": { 28 | "title": "Delete device?", 29 | "content": "Do you really want to delete the device?", 30 | "close": "Delete device", 31 | "dismiss": "Cancel" 32 | }, 33 | "dropdown-quick-actions": { 34 | "rename": "Rename", 35 | "on": "Turn on", 36 | "off": "Turn off", 37 | "delete": "Delete", 38 | "success-messages": { 39 | "copy": "Device copied to clipboard", 40 | "delete": "Device deleted", 41 | "duplicate": "Device duplicated", 42 | "cut": "Device cut to clipboard", 43 | "paste": "Device pasted", 44 | "pasteErrorMsg": "Clipboard is empty. Copy content first before pasting." 45 | } 46 | }, 47 | "device-details-header": { 48 | "title": "Device details", 49 | "firmware-card": "Firmware is not up to date", 50 | "update": "Update" 51 | }, 52 | "device-details-footer": { 53 | "set-maintenance": "Start maintenance", 54 | "end-maintenance": "End maintenance" 55 | }, 56 | "device-details": { 57 | "title": "Device information", 58 | "device-name": "Device name", 59 | "vendor": "Vendor", 60 | "device-type": "Device type", 61 | "description": "Device type", 62 | "status": "Status", 63 | "article-number": "Article number", 64 | "macaddress": "MAC address", 65 | "mac-address": "MAC address", 66 | "ipaddress": "IP address", 67 | "ip-address": "IP address", 68 | "firmware-version": "Firmware version", 69 | "serial-number": "Serial number", 70 | "quick-actions": "Quick actions" 71 | }, 72 | "incidents": { 73 | "title": "Incidents", 74 | "create-task": "Create task", 75 | "incident-cards": { 76 | "device": "Device", 77 | "date": "Date", 78 | "update": "Latest Update", 79 | "ipAddress": "IP Adress", 80 | "time": "Time", 81 | "info": "Information", 82 | "type": "Type" 83 | } 84 | }, 85 | "language": { 86 | "title": "Language", 87 | "de": "German", 88 | "en": "English" 89 | }, 90 | "list": "List", 91 | "overview": "Overview", 92 | "quick-actions": { 93 | "title": "Quick Actions", 94 | "add-device": "Add device", 95 | "add-devices": "Add devices", 96 | "schedule-maintenance": "Schedule maintenance" 97 | }, 98 | "status-history": { 99 | "title": "Status history" 100 | }, 101 | "settings": { 102 | "title": "Settings", 103 | "user-settings": "User settings", 104 | "application-settings": "Application settings", 105 | "notification": "Notifications", 106 | "toggle-on": "On", 107 | "toggle-off": "Off", 108 | "import-device": "Import device data", 109 | "upload-text": "Drag files here or...", 110 | "upload-button": "Upload files...", 111 | "theme": "Theme", 112 | "logout": "Logout" 113 | }, 114 | "toggle-theme": "Toggle theme", 115 | "search": "Search", 116 | "demo-message": "This feature is currently unavailable in the demo version.", 117 | "cancel": "Cancel", 118 | "okay": "OK", 119 | "user-settings": "User settings" 120 | } 121 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Code of Conduct 8 | 9 | ## Our Pledge 10 | 11 | In the interest of fostering an open and welcoming environment, we as 12 | contributors and maintainers pledge to make participation in our project and 13 | our community a harassment-free experience for everyone, regardless of age, body 14 | size, disability, ethnicity, sex characteristics, gender identity and expression, 15 | level of experience, education, socio-economic status, nationality, personal 16 | appearance, race, religion, or sexual identity and orientation. 17 | 18 | ## Our Standards 19 | 20 | Examples of behavior that contributes to creating a positive environment 21 | include: 22 | 23 | * Using welcoming and inclusive language 24 | * Being respectful of differing viewpoints and experiences 25 | * Gracefully accepting constructive criticism 26 | * Focusing on what is best for the community 27 | * Showing empathy towards other community members 28 | 29 | Examples of unacceptable behavior by participants include: 30 | 31 | * The use of sexualized language or imagery and unwelcome sexual attention or 32 | advances 33 | * Trolling, insulting/derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or electronic 36 | address, without explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Our Responsibilities 41 | 42 | Project maintainers are responsible for clarifying the standards of acceptable 43 | behavior and are expected to take appropriate and fair corrective action in 44 | response to any instances of unacceptable behavior. 45 | 46 | Project maintainers have the right and responsibility to remove, edit, or 47 | reject comments, commits, code, wiki edits, issues, and other contributions 48 | that are not aligned to this Code of Conduct, or to ban temporarily or 49 | permanently any contributor for other behaviors that they deem inappropriate, 50 | threatening, offensive, or harmful. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all project spaces, and it also applies when 55 | an individual is representing the project or its community in public spaces. 56 | Examples of representing a project or community include using an official 57 | project e-mail address, posting via an official social media account, or acting 58 | as an appointed representative at an online or offline event. Representation of 59 | a project may be further defined and clarified by project maintainers. 60 | 61 | This Code of Conduct also applies outside the project spaces when there is a 62 | reasonable belief that an individual's behavior may have a negative impact on 63 | the project or its community. 64 | 65 | ## Enforcement 66 | 67 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 68 | reported by contacting the project team at or . All 69 | complaints will be reviewed and investigated and will result in a response that 70 | is deemed necessary and appropriate to the circumstances. The project team is 71 | obligated to maintain confidentiality with regard to the reporter of an incident. 72 | Further details of specific enforcement policies may be posted separately. 73 | 74 | Project maintainers who do not follow or enforce the Code of Conduct in good 75 | faith may face temporary or permanent repercussions as determined by other 76 | members of the project's leadership. 77 | 78 | ## Attribution 79 | 80 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 81 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 82 | 83 | [homepage]: https://www.contributor-covenant.org 84 | 85 | For answers to common questions about this code of conduct, see 86 | https://www.contributor-covenant.org/faq 87 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/user-settings/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import useShowDemoMessage from "@/hooks/demoMessage"; 11 | import { themeSwitcher } from "@siemens/ix"; 12 | import { IxRadio, IxRadioGroup, IxTypography } from "@siemens/ix-react"; 13 | import clsx from "clsx"; 14 | import { useEffect, useState } from "react"; 15 | import { useTranslation } from "react-i18next"; 16 | import brand from "./brand.png"; 17 | import classic from "./classic.png"; 18 | import styles from "./styles.module.css"; 19 | 20 | function ThemeButton(props: { 21 | name: string; 22 | theme: string; 23 | active?: boolean; 24 | image: string; 25 | onClick?: () => void; 26 | }) { 27 | return ( 28 |
{ 31 | event.preventDefault(); 32 | event.stopPropagation(); 33 | 34 | if (props.onClick) { 35 | props.onClick(); 36 | } 37 | }} 38 | > 39 |
44 | Siemens brand theme 45 |
46 |
47 | { 51 | event.preventDefault(); 52 | console.log("ThemeButton onCheckedChange", props.theme); 53 | }} 54 | label={props.name} 55 | > 56 |
57 |
58 | ); 59 | } 60 | 61 | export default function UserSettings() { 62 | const showDemoMessage = useShowDemoMessage(); 63 | const { t, i18n } = useTranslation(); 64 | const [currentTheme, setCurrentTheme] = useState( 65 | import.meta.env.VITE_THEME ? "brand" : "classic", 66 | ); 67 | 68 | useEffect(() => { 69 | themeSwitcher.setTheme(currentTheme); 70 | }, [currentTheme]); 71 | 72 | function changeTheme(theme: string) { 73 | if (import.meta.env.VITE_THEME === undefined) { 74 | showDemoMessage(); 75 | return; 76 | } 77 | 78 | setCurrentTheme(theme); 79 | } 80 | 81 | return ( 82 |
83 | Theme 84 |
85 | changeTheme("brand")} 91 | /> 92 | changeTheme("classic")} 98 | /> 99 |
100 |
101 | 102 | {t("language.title")} 103 | 104 |
105 | 106 | i18n.changeLanguage("en")} 110 | label={t("language.en")} 111 | > 112 | i18n.changeLanguage("de")} 116 | label={t("language.de")} 117 | > 118 | 119 |
120 |
121 |
122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /apps/angular-starter/src/assets/i18n/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "analytics": "Analyse", 3 | "cards": "Karten", 4 | "content-header": "Hallo iX User, willkommen in unserer Demo-App!", 5 | "device-status": { 6 | "title": "Gerätestatus" 7 | }, 8 | "devices": "Geräte", 9 | "add-device": "Gerät hinzufügen", 10 | "device-quick-actions": { 11 | "add-device": "Gerät hinzufügen", 12 | "devices": "Geräte", 13 | "faulty-devices": "Fehler", 14 | "warning-devices": "Wartung" 15 | }, 16 | "category-filter": { 17 | "placeholder": "Filtern nach", 18 | "categories": "Kategorien" 19 | }, 20 | "device-add-modal": { 21 | "title": "Gerät hinzufügen", 22 | "list-header": "Bitte wählen sie einen Status", 23 | "close": "Gerät hinzufügen", 24 | "dismiss": "Abbrechen", 25 | "success": "Device erfolgreich hinzugefügt!" 26 | }, 27 | "device-delete-modal": { 28 | "title": "Gerät löschen?", 29 | "content": "Wollen Sie das Gerät wirklich löschen?", 30 | "close": "Gerät löschen", 31 | "dismiss": "Abbrechen" 32 | }, 33 | "dropdown-quick-actions": { 34 | "rename": "Umbennen", 35 | "on": "Einschalten", 36 | "off": "Ausschalten", 37 | "delete": "Löschen", 38 | "success-messages": { 39 | "copy": "Gerät erfolgreich in die Zwischenablage kopiert!", 40 | "delete": "Gerät erfolgreich entfernt!", 41 | "duplicate": "Gerät erfolgreich dupliziert!", 42 | "cut": "Gerät erfolgreich in die Zwischenablage übertragen!", 43 | "paste": "Gerät erfolgreich eingefügt!", 44 | "pasteErrorMsg": "Die Zwischenablage ist leer. Kopieren Sie den Inhalt, bevor Sie ihn einfügen." 45 | } 46 | }, 47 | "device-details-header": { 48 | "title": "Geräteinformationen", 49 | "firmware-card": "Gerätefirmware ist veraltet.", 50 | "update": "Update" 51 | }, 52 | "device-details-footer": { 53 | "set-maintenance": "Wartung starten", 54 | "end-maintenance": "Wartung beenden" 55 | }, 56 | "device-details": { 57 | "device-name": "Gerätename", 58 | "vendor": "Hersteller", 59 | "device-type": "Gerätetyp", 60 | "description": "Gerätetyp", 61 | "status": "Status", 62 | "article-number": "Artikelnummer", 63 | "macaddress": "MAC Adresse", 64 | "mac-address": "MAC Adresse", 65 | "ipaddress": "IP Adresse", 66 | "ip-address": "IP Adresse", 67 | "firmware-version": "Firmware-Version", 68 | "serial-number": "Serialnummer", 69 | "quick-actions": "Schnellaktionen" 70 | }, 71 | "incidents": { 72 | "title": "Zwischenfälle", 73 | "create-task": "Aufgabe erstellen", 74 | "incident-cards": { 75 | "device": "Gerät", 76 | "date": "Datum", 77 | "update": "Aktualisieren", 78 | "ipAddress": "IP Adresse", 79 | "time": "Zeit", 80 | "info": "Informationen", 81 | "type": "Typ" 82 | } 83 | }, 84 | "language": { 85 | "title": "Sprache", 86 | "de": "Deutsch", 87 | "en": "Englisch" 88 | }, 89 | "list": "Liste", 90 | "overview": "Übersicht", 91 | "quick-actions": { 92 | "title": "Quick Actions", 93 | "add-device": "Gerät hinzufügen", 94 | "add-devices": "Geräte hinzufügen", 95 | "schedule-maintenance": "Warung planen" 96 | }, 97 | "status-history": { 98 | "title": "Statusverlauf" 99 | }, 100 | "settings": { 101 | "title": "Einstellungen", 102 | "user-settings": "Benutzereinstellungen", 103 | "application-settings": "Anwendungseinstellungen", 104 | "notification": "Benachrichtigungen", 105 | "toggle-on": "Eingeschaltet", 106 | "toggle-off": "Ausgeschaltet", 107 | "import-device": "Gerätedaten importieren", 108 | "upload-text": "Dateien hierher ziehen oder...", 109 | "upload-button": "Dateien auswählen...", 110 | "theme": "Thema", 111 | "logout": "Abmelden" 112 | }, 113 | "toggle-theme": "Theme wechseln", 114 | "search": "Suchen", 115 | "demo-message": "Diese Funktion ist in der Demo-Version derzeit nicht verfügbar.", 116 | "cancel": "Abbrechen", 117 | "okay": "OK", 118 | "user-settings": "User settings" 119 | } 120 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/devices/components/device-details/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import { useSelectedDevice } from "@/store/hooks/device.ts"; 11 | import { Device } from "@/types/index.tsx"; 12 | import { toKebabCase } from "@/util/util.ts"; 13 | import { IxButton, IxDivider, IxPane, IxTypography } from "@siemens/ix-react"; 14 | import { useLayoutEffect, useState } from "react"; 15 | import { useTranslation } from "react-i18next"; 16 | import styles from "./styles.module.css"; 17 | import { useDataStore, useOverviewPaneStore } from "@/store/device-store"; 18 | 19 | const hideProperties = (key: string) => key !== "id"; 20 | 21 | const DeviceDetails = () => { 22 | const { editDevice } = useDataStore(); 23 | const selectedDevice = useSelectedDevice(); 24 | const [performMaintenance, setPerformMaintenance] = useState(false); 25 | const { t } = useTranslation(); 26 | const { expanded, setExpanded } = useOverviewPaneStore(); 27 | 28 | const isInMaintenance = selectedDevice?.status === "Maintenance"; 29 | 30 | useLayoutEffect(() => { 31 | const closeByEscape = (event: KeyboardEvent) => { 32 | if (expanded === false) { 33 | return; 34 | } 35 | 36 | if (event.key === "Escape") { 37 | setExpanded(false); 38 | } 39 | }; 40 | 41 | document.addEventListener("keydown", closeByEscape); 42 | 43 | return () => { 44 | document.removeEventListener("keydown", closeByEscape); 45 | }; 46 | }, [expanded, setExpanded]); 47 | 48 | return ( 49 | { 57 | setExpanded(event.detail.expanded); 58 | }} 59 | className={styles.Pane} 60 | > 61 |
62 | {selectedDevice ? ( 63 |
64 | 65 | {selectedDevice.deviceName} 66 | 67 | {Object.keys(selectedDevice) 68 | .filter(hideProperties) 69 | .map((key, index) => ( 70 |
71 | 72 | {t(`device-details.${toKebabCase(key)}`)} 73 | 74 | 75 | {selectedDevice[key as keyof Device]} 76 | 77 | 78 |
79 | ))} 80 |
81 | ) : ( 82 | 83 | No device selected 84 | 85 | )} 86 | 87 | { 91 | event.stopPropagation(); 92 | event.preventDefault(); 93 | 94 | setPerformMaintenance(true); 95 | setTimeout(() => { 96 | const updatedDevice = { 97 | ...selectedDevice!, 98 | status: isInMaintenance ? "Online" : "Maintenance", 99 | }; 100 | editDevice(updatedDevice as Device); 101 | setPerformMaintenance(false); 102 | }, 2000); 103 | }} 104 | > 105 | {!isInMaintenance && t("device-details-footer.set-maintenance")} 106 | {isInMaintenance && t("device-details-footer.end-maintenance")} 107 | 108 |
109 |
110 | ); 111 | }; 112 | 113 | export default DeviceDetails; 114 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # Siemens iX Starter 8 | 9 | Siemens iX Starter provides some starter applications. 10 | 11 | Contributions to the project and feedback are very welcome. 12 | 13 | ## Feature requests and bug reporting 14 | 15 | For feature requests as well as bug reports please open up an issue. 16 | 17 | ### Feature requests 18 | 19 | Feature requests need to include a clear description of the desired feature. Ideally a use case for the feature is also part of the request. Additional information like screenshots or design screens can increase understandability and cogency of the request. 20 | 21 | ### Bug reports 22 | 23 | Bug reports need to include a clear description of the problem as well as additional information that can help in solving the issue (e.g. software versions, stack traces). 24 | 25 | ## Merge requests 26 | 27 | If you are planning to contribute a larger change to iX it is advisable to open an issue for discussion first. This way you can make sure in advance that your changes will actually get accepted. 28 | 29 | In order to start working on an issue fork the `iX repositiory`. 30 | Once all changes are implemented commit them to the fork and create a merge request (MR). 31 | Please make sure to enable the option `Allow commits from members who can merge to the target branch` in your MR. This way the iX team will be able to rebase the MR if neccessary. 32 | 33 | Usually all changes have to be combined into one single commit for clarity. No unrelated changes must be part of that commit. 34 | If your MR should contain more than one commit it will get squash merged. 35 | 36 | ## Coding guidelines 37 | 38 | iX releases follow the concept of [Semantic versioning](https://semver.org/). Therefore it is vital that changes to the codebase don't violate these principles. 39 | The API of iX consists of components, TypeScript classes, CSS classes, CSS Custom properties and SCSS mixins. 40 | Please make sure that no accidental breaking changes are produced by your changes. 41 | For issues that are not yet tagged with a version number it is important that you communicate breaking changes in your merge request. 42 | 43 | In order to keep code quality high we ask for any contribution to adhere to any applicable guidelines listed below: 44 | 45 | - [Angular coding style guide](https://angular.io/guide/styleguide) 46 | - [Google's TypeScript style guide](https://google.github.io/styleguide/tsguide.html) 47 | 48 | ## Design specs/styleguide 49 | 50 | If you need access to iX design documentation please contact a iX maintainer. 51 | 52 | ## Definition of done (DoD) 53 | 54 | Any contribution will have to satisfy this list of criteria before it will get accepted: 55 | 56 | - Feature/fix as described in issue 57 | - Unit tests (where applicable) 58 | - Visual regression tests for components/styling 59 | - E2E tests (where applicable) 60 | - Example component/documentation for components/styling/concepts 61 | 62 | ## Commit guidlines 63 | 64 | #### Commit Message Header 65 | 66 | ``` 67 | (): 68 | │ │ │ 69 | │ │ └─⫸ Summary in present tense. Not capitalized. No period at the end. 70 | │ │ 71 | │ └─⫸ Commit Scope: animations|bazel|benchpress|common|compiler|compiler-cli|core| 72 | │ elements|forms|http|language-service|localize|platform-browser| 73 | │ platform-browser-dynamic|platform-server|router|service-worker| 74 | │ upgrade|zone.js|packaging|changelog|dev-infra|docs-infra|migrations| 75 | │ ngcc|ve 76 | │ 77 | └─⫸ Commit Type: build|ci|docs|feat|fix|perf|refactor|test 78 | ``` 79 | 80 | The `` and `` fields are mandatory, the `()` field is optional. 81 | 82 | ##### Type 83 | 84 | Must be one of the following: 85 | 86 | - **build**: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) 87 | - **ci**: Changes to our CI configuration files and scripts (example scopes: Circle, BrowserStack, SauceLabs) 88 | - **docs**: Documentation only changes 89 | - **feat**: A new feature 90 | - **fix**: A bug fix 91 | - **perf**: A code change that improves performance 92 | - **refactor**: A code change that neither fixes a bug nor adds a feature 93 | - **test**: Adding missing tests or correcting existing tests 94 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/devices/devices.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | {{ "add-device" | translate }} 10 | 11 | 12 | 13 |
14 |
15 | 23 | 24 |
25 | 31 | {{ statusCount.Online ?? "0" }} online 32 | 33 | 34 | 40 | {{ statusCount.Maintenance ?? "0" }} maintenance 41 | 42 | 43 | 49 | {{ statusCount.Error ?? "0" }} error 50 | 51 | 52 | 58 | {{ statusCount.Offline ?? "0" }} offline 59 | 60 |
61 |
62 | @if (!isFilterRowEmpty) { 63 |
64 | 77 | 78 |
79 | } @else { 80 |
81 | 88 |
89 | } 90 |
91 | 92 | 102 |
103 |
104 | 105 | {{ selectedRow.deviceName ?? "-" }} 106 | 107 | 108 | @for (item of selectedRowEntries; track item[0]; let i = $index) { 109 | 110 | {{ item[0] }} 111 | 112 | 113 | {{ item[1] || "-" }} 114 | 115 | 116 | } @empty { 117 |
  • No items available. {{ "language.title" | translate }}
  • 118 | } 119 |
    120 | 121 | 126 | {{ 127 | (selectedRow.status === "Maintenance" 128 | ? "device-details-footer.end-maintenance" 129 | : "device-details-footer.set-maintenance" 130 | ) | translate 131 | }} 132 | 133 |
    134 |
    135 |
    136 | -------------------------------------------------------------------------------- /apps/react-starter/src/pages/overview/components/device-range/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2024 Siemens AG 3 | * 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 10 | import styles from "./styles.module.css"; 11 | 12 | import { useEChartsTheme } from "@/hooks/theme"; 13 | import { IxCard, IxCardContent, IxTypography } from "@siemens/ix-react"; 14 | import { BarSeriesOption } from "echarts"; 15 | import { EChartsCoreOption } from "echarts"; 16 | import { useEffect, useRef, useState } from "react"; 17 | import { useTranslation } from "react-i18next"; 18 | import ReactEcharts from "echarts-for-react"; 19 | import { useResizeHandler } from "@/util/util.ts"; 20 | import EChartsReact from "echarts-for-react"; 21 | import { ECBasicOption } from "echarts/types/dist/shared"; 22 | import { getComputedCSSProperty } from "@siemens/ix-echarts"; 23 | import { Device, DeviceState } from "@/types"; 24 | import { useDataStore } from "@/store/device-store"; 25 | 26 | const reduceDevices = (devices: Device[]): BarSeriesOption[] => { 27 | const onlineData = new Map(); 28 | const offlineData = new Map(); 29 | const maintenanceData = new Map(); 30 | const errorData = new Map(); 31 | 32 | function fillData(device: Device, data: Map, state: DeviceState) { 33 | const ipSegment = device.ipAddress.split(".")[0] + ".x"; 34 | 35 | if (device.status !== state) { 36 | return; 37 | } 38 | 39 | if (data.has(ipSegment)) { 40 | data.set(ipSegment, data.get(ipSegment)! + 1); 41 | } else { 42 | data.set(ipSegment, 1); 43 | } 44 | } 45 | 46 | devices.forEach((device) => { 47 | fillData(device, onlineData, "Online"); 48 | fillData(device, maintenanceData, "Maintenance"); 49 | fillData(device, errorData, "Error"); 50 | fillData(device, offlineData, "Offline"); 51 | }); 52 | 53 | function createSeries(data: Map) { 54 | return Array.from(data).map(([name, value]) => [value, name]); 55 | } 56 | 57 | return [ 58 | { 59 | name: "Online", 60 | data: createSeries(onlineData), 61 | type: "bar", 62 | stack: "x", 63 | color: getComputedCSSProperty("color-success"), 64 | }, 65 | { 66 | name: "Maintenance", 67 | data: createSeries(maintenanceData), 68 | type: "bar", 69 | stack: "x", 70 | color: getComputedCSSProperty("color-warning"), 71 | }, 72 | { 73 | name: "Error", 74 | data: createSeries(errorData), 75 | type: "bar", 76 | stack: "x", 77 | color: getComputedCSSProperty("color-alarm"), 78 | }, 79 | { 80 | name: "Offline", 81 | data: createSeries(offlineData), 82 | type: "bar", 83 | stack: "x", 84 | color: getComputedCSSProperty("color-critical"), 85 | }, 86 | ]; 87 | }; 88 | 89 | function getOption(): EChartsCoreOption { 90 | return { 91 | tooltip: { 92 | trigger: "item", 93 | }, 94 | legend: { 95 | orient: "horizontal", 96 | icon: "rect", 97 | bottom: "0", 98 | left: "0", 99 | }, 100 | xAxis: { 101 | type: "value", 102 | name: "Devices", 103 | nameLocation: "middle", 104 | nameGap: 25, 105 | }, 106 | yAxis: { 107 | type: "category", 108 | name: "IP Range", 109 | nameLocation: "end", 110 | }, 111 | grid: { 112 | top: 45, 113 | bottom: 85, 114 | }, 115 | }; 116 | } 117 | 118 | function DeviceRange() { 119 | const { t } = useTranslation(); 120 | const chartRef = useRef(null); 121 | 122 | const [options, setOptions] = useState({}); 123 | 124 | const { devices } = useDataStore(); 125 | const theme = useEChartsTheme(); 126 | 127 | useEffect(() => { 128 | const chart = chartRef.current?.getEchartsInstance(); 129 | 130 | if (chart) { 131 | const data = reduceDevices(devices); 132 | const option = { 133 | ...getOption(), 134 | series: data, 135 | }; 136 | 137 | setOptions(option); 138 | } 139 | }, [devices]); 140 | 141 | useResizeHandler(chartRef); 142 | 143 | return ( 144 | 145 | 146 | 147 | {t("device-status.title")} 148 | 149 | setTimeout(echarts.resize)} 152 | className={styles.echarts} 153 | option={options} 154 | theme={theme} 155 | /> 156 | 157 | 158 | ); 159 | } 160 | 161 | export default DeviceRange; 162 | -------------------------------------------------------------------------------- /apps/angular-starter/src/app/pages/overview/components/incidents/incident-list/incident-list.component.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | 7 | {{ "incidents.incident-cards.type" | translate }} 8 | 9 | 10 | Incident name 11 | 12 | 13 | 14 | 15 | 21 | {{ "incidents.incident-cards.device" | translate }} 22 | 23 | 24 | 25 | 31 | {{ "incidents.incident-cards.date" | translate }} 32 | 33 | 34 | 35 | 36 | 37 | @for (incident of filteredIncidents; track incident.id) { 38 | 39 | 40 | @if (mediaQueryService.isMobile$ | async) { 41 | 42 | 43 | 44 | 45 | 46 | 47 | {{ incident.incidentName }} 48 | 49 | 50 | 51 | 52 | {{ incident.infoText }} 53 | 54 | 55 | 56 | 57 | 58 | {{ incident.deviceName }} 59 | 60 | 61 | {{ incident.ipAddress }} 62 | 63 | 64 | 65 | 70 | 71 | 72 | 73 | } @else { 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | {{ incident.incidentName }} 82 | 83 | 84 | 85 | 86 | {{ incident.infoText }} 87 | 88 | 89 | 90 | 91 | 92 | 93 | {{ incident.deviceName }} 94 | 95 | 96 | {{ incident.ipAddress }} 97 | 98 | 99 | 100 | 101 | 102 | {{ incident.date }} 103 | 104 | 105 | {{ incident.time }} 106 | 107 | 108 | 109 | 110 | 114 | 115 | {{ "incidents.create-task" | translate }} 116 | 117 | 118 | 119 | 120 | } 121 | 122 | } @empty { 123 |
    No items found.
    124 | } 125 |
    126 |
    127 | --------------------------------------------------------------------------------