@cryptotaxi247 / CoPilot / commits / e66dc65b

Cp dashboard (#780)

* feat: refine dashboard chart styling and responsiveness - Improve column chart readability by adding responsive breakpoints and adjusting x-axis label rotation. - Enhance pie chart donut labels with consistent typography and better positioning. - Implement safer data point selection logic using optional chaining to prevent potential errors. - Adjust histogram panel height to provide better spacing within the dashboard layout. * chore(deps): add apexcharts and dashboard utility dependencies - Add apexcharts and vue3-apexcharts to support the migration of dashboard components. - Add vue-i18n for internationalization support. - Add colord, detect-touch-device, and js-md5 utility libraries. * feat: initialize global provider and i18n support - Wrap the root component with a `Provider` to manage global layouts and UI state. - Register the i18n plugin to enable internationalization. - Inject the `naive-ui-style` meta tag into the document head to ensure correct CSS priority for Naive UI components. * feat: implement design token system and global theme management - Introduce `design-tokens.json` to centralize colors, typography, and layout constants for light and dark themes. - Implement a Pinia `theme` store to manage global UI state, including theme switching, sidebar behavior, and responsive overrides. - Add foundational SCSS architecture including global animations, router transitions, and component overrides for Naive UI and ApexCharts. - Provide utility functions for dynamic CSS variable generation and color manipulation. * feat: implement internationalization system and locale management - Setup `vue-i18n` configuration with initial support for German, English, Spanish, French, Italian, and Japanese. - Create a Pinia store to manage global locale state, persistence, and synchronization with `dayjs`. - Provide computed mappings to align the application locale with Naive UI's internal localization. * feat: enhance siem api type safety and add abort signal support - Introduce `DashboardPanelType` union to provide better type safety for dashboard panel definitions. - Update `getPanelData` to accept an optional `AbortSignal`, enabling the cancellation of pending API requests. * feat: refactor dashboard views and simplify routing structure - Update the dashboard detail route from `/dashboards/view/:id` to `/dashboards/:id`. - Extract dashboard rendering logic into a specialized `DashboardViewer` component. - Remove the manual customer selector UI from the dashboards listing page. - Transition dashboard navigation to use named routes and parameters. - Adjust customer code logic to auto-select the first available entry. * feat: implement core application layouts and global UI components - Introduce the `Blank` layout and `MainContainer` for flexible page structures. - Implement a comprehensive sidebar `Navbar` with categorized navigation for SIEM, Incident Management, and Agents. - Add a global `Toolbar` containing breadcrumbs, pinned pages, search, and system switches (theme, locale, fullscreen). - Setup `Provider` and `GlobalListener` to manage Naive UI configurations, notifications, and global actions. - Provide common UI elements including `Logo`, `MainFooter`, and `SplashScreen`. * feat: add new SVG icons and images for customer portal * feat: add utility functions for date formatting, byte conversion, and secure storage management - Introduce `dayjs` utility for date manipulation and formatting. - Implement `formatBytes` and `formatDate` functions for data presentation. - Create `secure-storage` module for secure local storage management with session support. - Add `highlighter` utility for code syntax highlighting with customizable themes. * feat: add new dashboard components and utilities - Introduce `Icon`, `CardLink`, and various chart components (`ChartBar`, `ChartColumn`, `ChartPie`) for enhanced dashboard visualization. - Implement `DashboardCategoriesSection`, `DashboardCategoryCard`, and `DashboardTemplateCard` for improved category and template management. - Add `EnabledDashboardsSection` for displaying and managing enabled dashboards. - Create utility functions for dashboard icon mapping and responsive column spans. - Establish a comprehensive `DashboardViewer` for displaying detailed dashboard panels. * feat: add new composables for enhanced functionality - Introduce `useBreadcrumb` for breadcrumb management with push, replace, update, pop, clear, setItems, and truncate functionalities. - Implement `useCustomerHealthcheckFilters` for managing healthcheck filter settings. - Create `useFullscreenSwitch` to toggle fullscreen mode. - Add `useGlobalActions` for centralized message and notification handling. - Develop `useHealthchecksNotify` to manage healthcheck notifications based on store state. - Implement `useHideLayoutFooter` to conditionally hide the layout footer based on theme settings. - Create `useLoadingBarSetup` for loading bar management during route transitions. - Introduce `useNavigation` for simplified routing and navigation actions. - Add `useNotifications` for managing notification state and actions. - Implement `useSearchDialog` for triggering search dialogs. - Create `useThemeSwitch` for toggling application themes. * feat: add main and settings stores with loading bar management - Introduce `useMainStore` for managing application state, including a loading bar and soft reload functionality. - Implement `useSettingsStore` to handle user settings such as date format and 24-hour time format, along with available date formats as getters. - Add type definitions for common and dashboard-related types to enhance type safety across the application. --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Mar 26, 2026 at 08:57 UTC e66dc65b9e18daa255b207c3fa7225c75ede5c28
108 files changed +6784 -401
customer_portal/package.json
+7 -1
@@ -24,10 +24,14 @@
24 "dependencies": {
25 "@ajoelp/json-to-formdata": "^1.5.0",
26 "@vueuse/core": "^14.1.0",
27 + "apexcharts": "^5.10.4",
28 "axios": "^1.13.2",
29 + "colord": "^2.9.3",
30 "dayjs": "^1.11.19",
31 + "detect-touch-device": "^1.1.6",
32 "echarts": "^6.0.0",
33 "jose": "^6.1.2",
34 + "js-md5": "^0.8.3",
35 "lodash": "^4.17.21",
36 "mitt": "^3.0.1",
37 "naive-ui": "^2.43.2",
@@ -36,7 +40,9 @@
40 "pinia-plugin-persistedstate": "^4.7.1",
41 "secure-ls": "^2.0.0",
42 "vue": "^3.5.25",
39 - "vue-router": "^4.6.3"
43 + "vue-i18n": "^11.3.0",
44 + "vue-router": "^4.6.3",
45 + "vue3-apexcharts": "^1.11.1"
46 },
47 "devDependencies": {
48 "@antfu/eslint-config": "^6.2.0",
customer_portal/pnpm-lock.yaml
+97
@@ -14,18 +14,30 @@ importers:
14 '@vueuse/core':
15 specifier: ^14.1.0
16 version: 14.1.0(vue@3.5.25(typescript@5.9.3))
17 + apexcharts:
18 + specifier: ^5.10.4
19 + version: 5.10.4
20 axios:
21 specifier: ^1.13.2
22 version: 1.13.2
23 + colord:
24 + specifier: ^2.9.3
25 + version: 2.9.3
26 dayjs:
27 specifier: ^1.11.19
28 version: 1.11.19
29 + detect-touch-device:
30 + specifier: ^1.1.6
31 + version: 1.1.6
32 echarts:
33 specifier: ^6.0.0
34 version: 6.0.0
35 jose:
36 specifier: ^6.1.2
37 version: 6.1.2
38 + js-md5:
39 + specifier: ^0.8.3
40 + version: 0.8.3
41 lodash:
42 specifier: ^4.17.21
43 version: 4.17.21
@@ -50,9 +62,15 @@ importers:
62 vue:
63 specifier: ^3.5.25
64 version: 3.5.25(typescript@5.9.3)
65 + vue-i18n:
66 + specifier: ^11.3.0
67 + version: 11.3.0(vue@3.5.25(typescript@5.9.3))
68 vue-router:
69 specifier: ^4.6.3
70 version: 4.6.3(vue@3.5.25(typescript@5.9.3))
71 + vue3-apexcharts:
72 + specifier: ^1.11.1
73 + version: 1.11.1(apexcharts@5.10.4)(vue@3.5.25(typescript@5.9.3))
74 devDependencies:
75 '@antfu/eslint-config':
76 specifier: ^6.2.0
@@ -599,6 +617,22 @@ packages:
617 peerDependencies:
618 vue: '>=3'
619
620 + '@intlify/core-base@11.3.0':
621 + resolution: {integrity: sha512-NNX5jIwF4TJBe7RtSKDMOA6JD9mp2mRcBHAwt2X+Q8PvnZub0yj5YYXlFu2AcESdgQpEv/5Yx2uOCV/yh7YkZg==}
622 + engines: {node: '>= 16'}
623 +
624 + '@intlify/devtools-types@11.3.0':
625 + resolution: {integrity: sha512-G9CNL4WpANWVdUjubOIIS7/D2j/0j+1KJmhBJxHilWNKr9mmt3IjFV3Hq4JoBP23uOoC5ynxz/FHZ42M+YxfGw==}
626 + engines: {node: '>= 16'}
627 +
628 + '@intlify/message-compiler@11.3.0':
629 + resolution: {integrity: sha512-RAJp3TMsqohg/Wa7bVF3cChRhecSYBLrTCQSj7j0UtWVFLP+6iEJoE2zb7GU5fp+fmG5kCbUdzhmlAUCWXiUJw==}
630 + engines: {node: '>= 16'}
631 +
632 + '@intlify/shared@11.3.0':
633 + resolution: {integrity: sha512-LC6P/uay7rXL5zZ5+5iRJfLs/iUN8apu9tm8YqQVmW3Uq3X4A0dOFUIDuAmB7gAC29wTHOS3EiN/IosNSz0eNQ==}
634 + engines: {node: '>= 16'}
635 +
636 '@jridgewell/gen-mapping@0.3.13':
637 resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
638
@@ -1194,6 +1228,9 @@ packages:
1228 resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
1229 engines: {node: '>=14'}
1230
1231 + apexcharts@5.10.4:
1232 + resolution: {integrity: sha512-gt0VUqZ2+mr25ScbUcKZgJr96jKYm4vjOcxEWCEh/E5F4dWqhyo3dBhPRvNNnkKiWxkMd2cBwj3ZYH3rK39fkA==}
1233 +
1234 are-docs-informative@0.0.2:
1235 resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==}
1236 engines: {node: '>=14'}
@@ -1312,6 +1349,9 @@ packages:
1349 color-name@1.1.4:
1350 resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
1351
1352 + colord@2.9.3:
1353 + resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
1354 +
1355 combined-stream@1.0.8:
1356 resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
1357 engines: {node: '>= 0.8'}
@@ -1446,6 +1486,9 @@ packages:
1486 resolution: {integrity: sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==}
1487 engines: {node: '>=8'}
1488
1489 + detect-touch-device@1.1.6:
1490 + resolution: {integrity: sha512-9DYLJE05EFGI9f8m/GyJtWjw7aMZMBQM2QVy6bb7zX8uC3iorOE3Erdrk3TWCj5FVOlHTfinU0bATTE9GWYebw==}
1491 +
1492 devlop@1.1.0:
1493 resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
1494
@@ -1958,6 +2001,9 @@ packages:
2001 jose@6.1.2:
2002 resolution: {integrity: sha512-MpcPtHLE5EmztuFIqB0vzHAWJPpmN1E6L4oo+kze56LIs3MyXIj9ZHMDxqOvkP38gBR7K1v3jqd4WU2+nrfONQ==}
2003
2004 + js-md5@0.8.3:
2005 + resolution: {integrity: sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ==}
2006 +
2007 js-tokens@4.0.0:
2008 resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
2009
@@ -2883,6 +2929,12 @@ packages:
2929 peerDependencies:
2930 eslint: ^8.57.0 || ^9.0.0
2931
2932 + vue-i18n@11.3.0:
2933 + resolution: {integrity: sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==}
2934 + engines: {node: '>= 16'}
2935 + peerDependencies:
2936 + vue: ^3.0.0
2937 +
2938 vue-router@4.6.3:
2939 resolution: {integrity: sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==}
2940 peerDependencies:
@@ -2894,6 +2946,12 @@ packages:
2946 peerDependencies:
2947 typescript: '>=5.0.0'
2948
2949 + vue3-apexcharts@1.11.1:
2950 + resolution: {integrity: sha512-MbN3vg8bMG19wc0Lm1HkeQvODgLm56DgpIxtNUO0xpf/JCzYWVGE4jzXp2JISzy2s3Kul1yOxNQUYsLvKQ5L9g==}
2951 + peerDependencies:
2952 + apexcharts: '>=5.10.0'
2953 + vue: '>=3.0.0'
2954 +
2955 vue@3.5.25:
2956 resolution: {integrity: sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==}
2957 peerDependencies:
@@ -3431,6 +3489,24 @@ snapshots:
3489 '@iconify/types': 2.0.0
3490 vue: 3.5.25(typescript@5.9.3)
3491
3492 + '@intlify/core-base@11.3.0':
3493 + dependencies:
3494 + '@intlify/devtools-types': 11.3.0
3495 + '@intlify/message-compiler': 11.3.0
3496 + '@intlify/shared': 11.3.0
3497 +
3498 + '@intlify/devtools-types@11.3.0':
3499 + dependencies:
3500 + '@intlify/core-base': 11.3.0
3501 + '@intlify/shared': 11.3.0
3502 +
3503 + '@intlify/message-compiler@11.3.0':
3504 + dependencies:
3505 + '@intlify/shared': 11.3.0
3506 + source-map-js: 1.2.1
3507 +
3508 + '@intlify/shared@11.3.0': {}
3509 +
3510 '@jridgewell/gen-mapping@0.3.13':
3511 dependencies:
3512 '@jridgewell/sourcemap-codec': 1.5.5
@@ -4034,6 +4110,8 @@ snapshots:
4110
4111 ansis@4.2.0: {}
4112
4113 + apexcharts@5.10.4: {}
4114 +
4115 are-docs-informative@0.0.2: {}
4116
4117 argparse@2.0.1: {}
@@ -4144,6 +4222,8 @@ snapshots:
4222
4223 color-name@1.1.4: {}
4224
4225 + colord@2.9.3: {}
4226 +
4227 combined-stream@1.0.8:
4228 dependencies:
4229 delayed-stream: 1.0.0
@@ -4258,6 +4338,8 @@ snapshots:
4338
4339 detect-libc@2.1.0: {}
4340
4341 + detect-touch-device@1.1.6: {}
4342 +
4343 devlop@1.1.0:
4344 dependencies:
4345 dequal: 2.0.3
@@ -4815,6 +4897,8 @@ snapshots:
4897
4898 jose@6.1.2: {}
4899
4900 + js-md5@0.8.3: {}
4901 +
4902 js-tokens@4.0.0: {}
4903
4904 js-yaml@4.1.0:
@@ -5824,6 +5908,14 @@ snapshots:
5908 transitivePeerDependencies:
5909 - supports-color
5910
5911 + vue-i18n@11.3.0(vue@3.5.25(typescript@5.9.3)):
5912 + dependencies:
5913 + '@intlify/core-base': 11.3.0
5914 + '@intlify/devtools-types': 11.3.0
5915 + '@intlify/shared': 11.3.0
5916 + '@vue/devtools-api': 6.6.4
5917 + vue: 3.5.25(typescript@5.9.3)
5918 +
5919 vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)):
5920 dependencies:
5921 '@vue/devtools-api': 6.6.4
@@ -5835,6 +5927,11 @@ snapshots:
5927 '@vue/language-core': 3.1.5(typescript@5.9.3)
5928 typescript: 5.9.3
5929
5930 + vue3-apexcharts@1.11.1(apexcharts@5.10.4)(vue@3.5.25(typescript@5.9.3)):
5931 + dependencies:
5932 + apexcharts: 5.10.4
5933 + vue: 3.5.25(typescript@5.9.3)
5934 +
5935 vue@3.5.25(typescript@5.9.3):
5936 dependencies:
5937 '@vue/compiler-dom': 3.5.25
customer_portal/src/App.vue
+4 -1
@@ -1,9 +1,12 @@
1 <template>
2 - <router-view />
2 + <Provider>
3 + <router-view />
4 + </Provider>
5 </template>
6
7 <script setup lang="ts">
8 import { onMounted, watch } from "vue"
9 +import Provider from "@/app-layouts/common/Provider.vue"
10 import { usePortalSettingsStore } from "./stores/portalSettings"
11
12 const portalSettingsStore = usePortalSettingsStore()
customer_portal/src/api/siem.ts
+15 -3
@@ -21,10 +21,12 @@ export interface FieldMapping {
21 type: string
22 }
23
24 +export type DashboardPanelType = "stat" | "pie" | "bar_h" | "histogram"
25 +
26 export interface DashboardPanel {
27 id: string
28 title: string
27 - type: string
29 + type: DashboardPanelType
30 w: number
31 h: number
32 lucene: string
@@ -120,7 +122,17 @@ export class SiemAPI {
122 return (await httpClient.get(`/siem/dashboards/enabled/${customerCode}`)).data
123 }
124
123 - static async getPanelData(dashboardId: number, timerange: string): Promise<PanelDataResponse> {
124 - return (await httpClient.post(`/siem/dashboards/panel-data`, { dashboard_id: dashboardId, timerange })).data
125 + static async getPanelData(
126 + dashboardId: number,
127 + timerange: string,
128 + signal?: AbortSignal
129 + ): Promise<PanelDataResponse> {
130 + return (
131 + await httpClient.post(
132 + `/siem/dashboards/panel-data`,
133 + { dashboard_id: dashboardId, timerange },
134 + signal ? { signal } : {}
135 + )
136 + ).data
137 }
138 }
customer_portal/src/app-layouts/Blank/Blank.vue new
+22
@@ -0,0 +1,22 @@
1 +<template>
2 + <div class="layout flex">
3 + <MainContainer class="grow">
4 + <slot />
5 + </MainContainer>
6 + </div>
7 +</template>
8 +
9 +<script lang="ts" setup>
10 +import MainContainer from "./MainContainer.vue"
11 +import "./main.scss"
12 +</script>
13 +
14 +<style lang="scss" scoped>
15 +.layout {
16 + width: 100vw;
17 + height: 100vh;
18 + height: 100svh;
19 + overflow: hidden;
20 + perspective: 1000px;
21 +}
22 +</style>
customer_portal/src/app-layouts/Blank/MainContainer.vue new
+78
@@ -0,0 +1,78 @@
1 +<template>
2 + <div id="app-main" class="main">
3 + <n-scrollbar ref="scrollbar">
4 + <div
5 + id="app-view"
6 + class="view"
7 + :class="{ boxed, 'view-padded': overridePadded === true, 'view-no-padded': overridePadded === false }"
8 + >
9 + <slot />
10 + </div>
11 + </n-scrollbar>
12 + </div>
13 +</template>
14 +
15 +<script lang="ts" setup>
16 +import type { RouteLocationNormalizedGeneric } from "vue-router"
17 +import { NScrollbar } from "naive-ui"
18 +import { computed, onMounted, ref } from "vue"
19 +import { useRoute, useRouter } from "vue-router"
20 +import { useThemeStore } from "@/stores/theme"
21 +
22 +const themeStore = useThemeStore()
23 +const router = useRouter()
24 +const route = useRoute()
25 +const themeBoxed = computed<boolean>(() => themeStore.isBoxed)
26 +const overrideBoxed = ref<boolean | undefined>(undefined)
27 +const overridePadded = ref<boolean | undefined>(undefined)
28 +const boxed = computed<boolean>(() => (overrideBoxed.value !== undefined ? overrideBoxed.value : themeBoxed.value))
29 +const scrollbar = ref()
30 +
31 +function checkThemeOverrides(currentRoute: RouteLocationNormalizedGeneric) {
32 + if (currentRoute.meta?.theme?.boxed?.enabled !== undefined) {
33 + overrideBoxed.value = currentRoute.meta.theme.boxed.enabled
34 + } else {
35 + overrideBoxed.value = undefined
36 + }
37 +
38 + if (currentRoute.meta?.theme?.padded?.enabled !== undefined) {
39 + overridePadded.value = currentRoute.meta.theme.padded.enabled
40 + } else {
41 + overridePadded.value = undefined
42 + }
43 +}
44 +
45 +router.beforeEach(checkThemeOverrides)
46 +
47 +onMounted(() => {
48 + checkThemeOverrides(route)
49 +
50 + router.afterEach(() => {
51 + if (scrollbar?.value?.scrollTo) {
52 + scrollbar?.value.scrollTo({ top: 0, behavior: "smooth" })
53 + }
54 + })
55 +})
56 +</script>
57 +
58 +<style lang="scss" scoped>
59 +.main {
60 + width: 100%;
61 + position: relative;
62 + background-color: var(--bg-body-color);
63 +
64 + .view {
65 + padding: 0 var(--view-padding);
66 + flex-grow: 1;
67 + width: 100%;
68 + margin: 0 auto;
69 +
70 + &.boxed {
71 + max-width: var(--boxed-width);
72 + }
73 + &.view-no-padded {
74 + padding: 0;
75 + }
76 + }
77 +}
78 +</style>
customer_portal/src/app-layouts/Blank/index.ts new
+1
@@ -0,0 +1 @@
1 +export { default } from "./Blank.vue"
customer_portal/src/app-layouts/Blank/main.scss new
+8
@@ -0,0 +1,8 @@
1 +.layout-Blank {
2 + .page-wrapped {
3 + height: 100svh;
4 + }
5 + .page-min-wrapped {
6 + min-height: 100svh;
7 + }
8 +}
customer_portal/src/app-layouts/common/GlobalListener.vue new
+15
@@ -0,0 +1,15 @@
1 +<template>
2 + <slot />
3 +</template>
4 +
5 +<script setup lang="ts">
6 +import { useMessage, useNotification } from "naive-ui"
7 +import { useGlobalActions } from "@/composables/useGlobalActions"
8 +import { useLoadingBarSetup } from "@/composables/useLoadingBarSetup"
9 +
10 +const message = useMessage()
11 +const notification = useNotification()
12 +
13 +useGlobalActions().init({ message, notification })
14 +useLoadingBarSetup()
15 +</script>
customer_portal/src/app-layouts/common/Logo.vue new
+68
@@ -0,0 +1,68 @@
1 +<template>
2 + <div class="logo">
3 + <Transition name="logo-fade" mode="out-in">
4 + <img
5 + :key="logoSrc"
6 + :src="logoSrc"
7 + alt="SOCFortress logo"
8 + aria-label="SOCFortress logo"
9 + class="logo-image"
10 + />
11 + </Transition>
12 + </div>
13 +</template>
14 +
15 +<script lang="ts" setup>
16 +import { computed } from "vue"
17 +import { useThemeStore } from "@/stores/theme"
18 +
19 +interface Props {
20 + type?: "default" | "small" | "large"
21 + dark?: boolean
22 + maxHeight?: string
23 +}
24 +
25 +const props = withDefaults(defineProps<Props>(), {
26 + type: "default",
27 + dark: undefined,
28 + maxHeight: "32px"
29 +})
30 +
31 +const themeStore = useThemeStore()
32 +
33 +const isDark = computed<boolean>(() => props.dark ?? themeStore.isThemeDark)
34 +
35 +const logoSrc = computed<string>(() => {
36 + const theme = isDark.value ? "dark" : "light"
37 + return new URL(`../../assets/images/socfortress_logo_${props.type}_${theme}.svg`, import.meta.url).href
38 +})
39 +</script>
40 +
41 +<style lang="scss" scoped>
42 +.logo {
43 + height: 100%;
44 + display: flex;
45 + align-items: center;
46 + justify-content: center;
47 + position: relative;
48 +
49 + .logo-image {
50 + max-height: v-bind(maxHeight);
51 + height: 100%;
52 + width: auto;
53 + display: block;
54 + object-fit: contain;
55 + }
56 +}
57 +
58 +// Smooth transition between theme changes
59 +.logo-fade-enter-active,
60 +.logo-fade-leave-active {
61 + transition: opacity 0.2s ease-in-out;
62 +}
63 +
64 +.logo-fade-enter-from,
65 +.logo-fade-leave-to {
66 + opacity: 0;
67 +}
68 +</style>
customer_portal/src/app-layouts/common/MainFooter.vue new
+81
@@ -0,0 +1,81 @@
1 +<template>
2 + <footer class="footer py-4" :class="{ boxed }">
3 + <div class="wrap flex items-center justify-end gap-3">
4 + <div class="copy">
5 + Made with
6 + <Icon :size="22" class="text-primary mx-1">
7 + <BrainIcon />
8 + </Icon>
9 + By
10 + <a
11 + href="https://www.socfortress.co/"
12 + target="_blank"
13 + alt="SOCFortress"
14 + rel="noopener noreferrer"
15 + class="mx-1"
16 + >
17 + SOCFortress
18 + </a>
19 + All rights Reserved © Copyright {{ year }}
20 + </div>
21 + </div>
22 + </footer>
23 +</template>
24 +
25 +<script lang="ts" setup>
26 +import { ref } from "vue"
27 +import BrainIcon from "@/assets/icons/brain-icon.svg"
28 +import Icon from "@/components/common/Icon.vue"
29 +
30 +const { boxed } = defineProps<{
31 + boxed: boolean
32 +}>()
33 +
34 +const year = ref(new Date().getFullYear())
35 +</script>
36 +
37 +<style lang="scss" scoped>
38 +.footer {
39 + width: 100%;
40 + max-width: 100%;
41 + overflow: hidden;
42 + font-size: 13px;
43 +
44 + .wrap {
45 + overflow: hidden;
46 + width: 100%;
47 + max-width: 100%;
48 + margin: 0 auto;
49 + padding: 0 var(--view-padding);
50 +
51 + .copy {
52 + line-height: 1.6;
53 +
54 + a {
55 + font-weight: bold;
56 + text-decoration: none;
57 + }
58 + * {
59 + display: inline;
60 + }
61 + i {
62 + display: inline-block;
63 + }
64 + }
65 + }
66 +
67 + &.boxed {
68 + .wrap {
69 + max-width: var(--boxed-width);
70 + }
71 + }
72 +
73 + @media (max-width: 700px) {
74 + font-size: 10px;
75 +
76 + i.n-icon {
77 + font-size: 18px !important;
78 + }
79 + }
80 +}
81 +</style>
customer_portal/src/app-layouts/common/Navbar/Navbar.vue new
+204
@@ -0,0 +1,204 @@
1 +<template>
2 + <nav class="nav" :class="{ collapsed }">
3 + <n-menu
4 + ref="menu"
5 + v-model:value="selectedKey"
6 + :options="menuOptions"
7 + :collapsed
8 + :indent="18"
9 + accordion
10 + :collapsed-width
11 + :dropdown-props="{
12 + scrollable: true,
13 + menuProps: () => ({
14 + class: 'main-nav'
15 + })
16 + }"
17 + :expanded-keys
18 + @update:expanded-keys="handleUpdateExpandedKeys"
19 + />
20 + </nav>
21 +
22 + <div class="hidden">
23 + <StackProvisioningButton ref="stackProvisioningButton" />
24 + <ActiveResponseWizardButton ref="activeResponseWizardButton" />
25 + <ThreatIntelButton ref="threatIntelButton" />
26 + </div>
27 +</template>
28 +
29 +<script lang="ts" setup>
30 +import type { MenuInst } from "naive-ui"
31 +import type { MenuMixedOption } from "naive-ui/es/menu/src/interface"
32 +import type { RouteRecordNormalized } from "vue-router"
33 +import _uniq from "lodash/uniq"
34 +import { NMenu } from "naive-ui"
35 +import { computed, onBeforeMount, ref, watch } from "vue"
36 +import { useRoute, useRouter } from "vue-router"
37 +import ActiveResponseWizardButton from "@/components/activeResponse/ActiveResponseWizardButton.vue"
38 +import StackProvisioningButton from "@/components/stackProvisioning/StackProvisioningButton.vue"
39 +import ThreatIntelButton from "@/components/threatIntel/ThreatIntelButton.vue"
40 +import { useThemeStore } from "@/stores/theme"
41 +
42 +import getItems from "./items"
43 +
44 +const { collapsed = false } = defineProps<{
45 + collapsed?: boolean
46 +}>()
47 +
48 +const route = useRoute()
49 +const router = useRouter()
50 +const selectedKey = ref<string | null>(null)
51 +const menu = ref<MenuInst | null>(null)
52 +const expandedKeys = ref<string[] | undefined>(undefined)
53 +
54 +const themeStore = useThemeStore()
55 +const menuOptions = computed<MenuMixedOption[]>(() => getItems())
56 +const collapsedWidth = computed<number>(() => themeStore.sidebar.closeWidth)
57 +const sidebarCollapsed = computed<boolean>(() => themeStore.sidebar.collapsed)
58 +
59 +const stackProvisioningButton = ref<InstanceType<typeof StackProvisioningButton> | null>(null)
60 +const threatIntelButton = ref<InstanceType<typeof ThreatIntelButton> | null>(null)
61 +const activeResponseWizardButton = ref<InstanceType<typeof ActiveResponseWizardButton> | null>(null)
62 +
63 +watch(selectedKey, val => {
64 + handleMenuSelect(val)
65 +})
66 +
67 +function handleMenuSelect(key: string | null) {
68 + switch (key) {
69 + case "Tools-ThreatIntel":
70 + threatIntelButton.value?.openDrawer()
71 + break
72 + case "Tools-StackProvisioning":
73 + stackProvisioningButton.value?.openModal()
74 + break
75 + case "Tools-ActiveResponse":
76 + activeResponseWizardButton.value?.openModal()
77 + break
78 + }
79 + selectedKey.value = key
80 +}
81 +
82 +function setMenuKey(matched: RouteRecordNormalized[]) {
83 + for (const match of matched) {
84 + if (match.name && typeof match.name === "string") {
85 + selectedKey.value = match.name?.toString() || null
86 + if (selectedKey.value) {
87 + menu.value?.showOption(selectedKey.value)
88 + }
89 + }
90 + }
91 +}
92 +
93 +onBeforeMount(() => {
94 + setMenuKey(route.matched)
95 +
96 + router.afterEach(route => {
97 + if (route?.matched?.length) {
98 + setMenuKey(route.matched)
99 +
100 + if (window.innerWidth <= 700 && !sidebarCollapsed.value) {
101 + themeStore.closeSidebar()
102 + }
103 + }
104 + })
105 +})
106 +
107 +function handleUpdateExpandedKeys(value: string[]) {
108 + const submenu = "components"
109 +
110 + if (value?.length && value.includes(submenu)) {
111 + const lastKey = value.pop()
112 + if (lastKey) {
113 + expandedKeys.value = _uniq([submenu, lastKey])
114 + }
115 + } else {
116 + expandedKeys.value = undefined
117 + }
118 +}
119 +</script>
120 +
121 +<style lang="scss" scoped>
122 +.nav {
123 + &.collapsed {
124 + pointer-events: none;
125 + }
126 +
127 + :deep() {
128 + .n-menu-item-group {
129 + .n-menu-item-group-title {
130 + white-space: nowrap;
131 + overflow: hidden;
132 + text-overflow: ellipsis;
133 + }
134 + }
135 +
136 + .n-submenu-children {
137 + --dash-width: 8px;
138 + --dash-height: 2px;
139 + --dash-offset: 29px;
140 +
141 + position: relative;
142 +
143 + &::before {
144 + content: "";
145 + display: block;
146 + background-color: var(--border-color);
147 + width: var(--dash-height);
148 + position: absolute;
149 + top: 0px;
150 + bottom: 20px;
151 + left: var(--dash-offset);
152 + }
153 +
154 + .n-menu-item-content {
155 + &::after {
156 + content: "";
157 + display: block;
158 + background-color: var(--border-color);
159 + width: var(--dash-width);
160 + height: var(--dash-height);
161 + position: absolute;
162 + z-index: -1;
163 + top: calc(50% - calc(ar(--dash-height) / 2));
164 + left: calc(var(--dash-offset) + var(--dash-height));
165 + }
166 + }
167 +
168 + .n-menu-item-group {
169 + .n-menu-item-group-title {
170 + padding-left: 44px !important;
171 + }
172 + }
173 +
174 + .n-submenu-children {
175 + &::before {
176 + display: none;
177 + }
178 + .n-menu-item-content {
179 + &::after {
180 + width: calc(var(--dash-width) * 3);
181 + background: repeating-linear-gradient(
182 + 90deg,
183 + var(--border-color) 0px,
184 + var(--border-color) 5px,
185 + transparent 5px,
186 + transparent 8px
187 + );
188 + }
189 + }
190 + }
191 + }
192 + }
193 +}
194 +
195 +.direction-rtl {
196 + .nav {
197 + :deep() {
198 + .n-submenu-children {
199 + --dash-offset: 25px;
200 + }
201 + }
202 + }
203 +}
204 +</style>
customer_portal/src/app-layouts/common/Navbar/index.ts new
+1
@@ -0,0 +1 @@
1 +export { default } from "./Navbar.vue"
customer_portal/src/app-layouts/common/Navbar/items.tsx new
+651
@@ -0,0 +1,651 @@
1 +import type { MenuMixedOption } from "naive-ui/es/menu/src/interface"
2 +import { h } from "vue"
3 +import { RouterLink } from "vue-router"
4 +import IncidentManagementIcon from "@/assets/icons/alert-settings-icon.svg"
5 +
6 +import { renderIcon } from "@/utils"
7 +
8 +const OverviewIcon = "carbon:dashboard"
9 +const IndiciesIcon = "ph:list-magnifying-glass"
10 +const AgentsIcon = "carbon:network-3"
11 +const ConnectorsIcon = "carbon:hybrid-networking"
12 +const GraylogIcon = "majesticons:pulse-line"
13 +const AlertsIcon = "carbon:warning-hex"
14 +const ArtifactsIcon = "carbon:document-multiple-01"
15 +// const SocIcon = "carbon:security"
16 +const HealthcheckIcon = "ph:heartbeat"
17 +const CustomersIcon = "carbon:user-multiple"
18 +const ExternalServicesIcon = "carbon:ibm-cloud-direct-link-2-dedicated"
19 +const ReportCreationIcon = "carbon:report-data"
20 +const SchedulerIcon = "material-symbols:autoplay"
21 +const CustomerPortalIcon = "streamline-ultimate:coding-apps-website-apps-browser"
22 +const ToolsIcon = "carbon:tool-box"
23 +// const EventSearchIcon = "carbon:search-locate"
24 +
25 +// TODO-FE: refactor
26 +export default function getItems(): MenuMixedOption[] {
27 + return [
28 + {
29 + label: () =>
30 + h(
31 + RouterLink,
32 + {
33 + to: {
34 + name: "Overview"
35 + }
36 + },
37 + { default: () => "Overview" }
38 + ),
39 + key: "Overview",
40 + icon: renderIcon(OverviewIcon)
41 + },
42 + {
43 + label: "Incident Management",
44 + key: "IncidentManagement",
45 + icon: renderIcon(IncidentManagementIcon),
46 + children: [
47 + {
48 + label: () =>
49 + h(
50 + RouterLink,
51 + {
52 + to: {
53 + name: "IncidentManagement-Sources"
54 + }
55 + },
56 + { default: () => "Sources" }
57 + ),
58 + key: "IncidentManagement-Sources"
59 + },
60 + {
61 + label: () =>
62 + h(
63 + RouterLink,
64 + {
65 + to: {
66 + name: "IncidentManagement-Alerts"
67 + }
68 + },
69 + { default: () => "Alerts" }
70 + ),
71 + key: "IncidentManagement-Alerts"
72 + },
73 + {
74 + label: () =>
75 + h(
76 + RouterLink,
77 + {
78 + to: {
79 + name: "IncidentManagement-Cases"
80 + }
81 + },
82 + { default: () => "Cases" }
83 + ),
84 + key: "IncidentManagement-Cases"
85 + }
86 + /*
87 + {
88 + label: () =>
89 + h(
90 + RouterLink,
91 + {
92 + to: {
93 + name: "IncidentManagement-Sigma"
94 + }
95 + },
96 + { default: () => "SIGMA" }
97 + ),
98 + key: "IncidentManagement-Sigma"
99 + }
100 + */
101 + ]
102 + },
103 + {
104 + label: "SIEM",
105 + key: "SIEM",
106 + icon: renderIcon(AlertsIcon),
107 + children: [
108 + {
109 + label: () =>
110 + h(
111 + RouterLink,
112 + {
113 + to: {
114 + name: "Alerts-SIEM"
115 + }
116 + },
117 + { default: () => "Alerts" }
118 + ),
119 + key: "Alerts-SIEM"
120 + },
121 + {
122 + label: () =>
123 + h(
124 + RouterLink,
125 + {
126 + to: {
127 + name: "EventSearch"
128 + }
129 + },
130 + { default: () => "Event Search" }
131 + ),
132 + key: "EventSearch"
133 + },
134 + {
135 + label: () =>
136 + h(
137 + RouterLink,
138 + {
139 + to: {
140 + name: "Dashboards"
141 + }
142 + },
143 + { default: () => "Dashboards" }
144 + ),
145 + key: "Dashboards"
146 + },
147 + {
148 + label: () =>
149 + h(
150 + RouterLink,
151 + {
152 + to: {
153 + name: "Alerts-Mitre"
154 + }
155 + },
156 + { default: () => "MITRE ATT&CK" }
157 + ),
158 + key: "Alerts-Mitre"
159 + },
160 + {
161 + label: () =>
162 + h(
163 + RouterLink,
164 + {
165 + to: {
166 + name: "Alerts-AtomicRedTeam"
167 + }
168 + },
169 + { default: () => "Atomic Red Team" }
170 + ),
171 + key: "Alerts-AtomicRedTeam"
172 + }
173 + ]
174 + },
175 + {
176 + label: () =>
177 + h(
178 + RouterLink,
179 + {
180 + to: {
181 + name: "Artifacts"
182 + }
183 + },
184 + { default: () => "Artifacts" }
185 + ),
186 + key: "Artifacts",
187 + icon: renderIcon(ArtifactsIcon)
188 + },
189 + {
190 + label: () =>
191 + h(
192 + RouterLink,
193 + {
194 + to: {
195 + name: "Customers"
196 + }
197 + },
198 + { default: () => "Customers" }
199 + ),
200 + key: "Customers",
201 + icon: renderIcon(CustomersIcon)
202 + },
203 + {
204 + label: "Agents",
205 + key: "Agents",
206 + icon: renderIcon(AgentsIcon),
207 + children: [
208 + {
209 + label: () =>
210 + h(
211 + RouterLink,
212 + {
213 + to: {
214 + name: "Agents"
215 + }
216 + },
217 + { default: () => "Agents list" }
218 + ),
219 + key: "Agents"
220 + },
221 + {
222 + label: () =>
223 + h(
224 + RouterLink,
225 + {
226 + to: {
227 + name: "Groups"
228 + }
229 + },
230 + { default: () => "Groups" }
231 + ),
232 + key: "Groups"
233 + },
234 + {
235 + label: () =>
236 + h(
237 + RouterLink,
238 + {
239 + to: {
240 + name: "SysmonConfig"
241 + }
242 + },
243 + { default: () => "Sysmon Config" }
244 + ),
245 + key: "SysmonConfig"
246 + },
247 + {
248 + label: () =>
249 + h(
250 + RouterLink,
251 + {
252 + to: {
253 + name: "DetectionRules"
254 + }
255 + },
256 + { default: () => "Detection Rules" }
257 + ),
258 + key: "DetectionRules"
259 + },
260 + {
261 + label: () =>
262 + h(
263 + RouterLink,
264 + {
265 + to: {
266 + name: "CopilotActions"
267 + }
268 + },
269 + { default: () => "CoPilot Actions" }
270 + ),
271 + key: "CopilotActions"
272 + },
273 + {
274 + label: () =>
275 + h(
276 + RouterLink,
277 + {
278 + to: {
279 + name: "CopilotSearches"
280 + }
281 + },
282 + { default: () => "CoPilot Searches" }
283 + ),
284 + key: "CopilotSearches"
285 + },
286 + {
287 + label: () =>
288 + h(
289 + RouterLink,
290 + {
291 + to: {
292 + name: "VulnerabilityOverview"
293 + }
294 + },
295 + { default: () => "Vulnerability Overview" }
296 + ),
297 + key: "VulnerabilityOverview"
298 + },
299 + {
300 + label: () =>
301 + h(
302 + RouterLink,
303 + {
304 + to: {
305 + name: "PatchTuesday"
306 + }
307 + },
308 + { default: () => "Patch Tuesday" }
309 + ),
310 + key: "PatchTuesday"
311 + },
312 + {
313 + label: "Security Configuration Assessment",
314 + key: "SCA",
315 + children: [
316 + {
317 + label: () =>
318 + h(
319 + RouterLink,
320 + {
321 + to: {
322 + name: "ScaOverview"
323 + }
324 + },
325 + { default: () => "SCA Overview" }
326 + ),
327 + key: "ScaOverview"
328 + },
329 + {
330 + label: () =>
331 + h(
332 + RouterLink,
333 + {
334 + to: {
335 + name: "ScaPolicies"
336 + }
337 + },
338 + { default: () => "SCA Policies" }
339 + ),
340 + key: "ScaPolicies"
341 + }
342 + ]
343 + }
344 + ]
345 + },
346 + {
347 + label: "Report Creation",
348 + key: "ReportCreation",
349 + icon: renderIcon(ReportCreationIcon),
350 + children: [
351 + {
352 + label: () =>
353 + h(
354 + RouterLink,
355 + {
356 + to: {
357 + name: "ReportCreation"
358 + }
359 + },
360 + { default: () => "General Reports" }
361 + ),
362 + key: "ReportCreation"
363 + },
364 + {
365 + label: () =>
366 + h(
367 + RouterLink,
368 + {
369 + to: {
370 + name: "VulnerabilityReports"
371 + }
372 + },
373 + { default: () => "Vulnerability Reports" }
374 + ),
375 + key: "VulnerabilityReports"
376 + },
377 + {
378 + label: () =>
379 + h(
380 + RouterLink,
381 + {
382 + to: {
383 + name: "SCAReports"
384 + }
385 + },
386 + { default: () => "SCA Reports" }
387 + ),
388 + key: "SCAReports"
389 + }
390 + ]
391 + },
392 + {
393 + label: "Healthcheck",
394 + key: "HealthcheckMenu",
395 + icon: renderIcon(HealthcheckIcon),
396 + children: [
397 + {
398 + label: () =>
399 + h(
400 + RouterLink,
401 + {
402 + to: {
403 + name: "Healthcheck"
404 + }
405 + },
406 + { default: () => "Healthcheck Alerts" }
407 + ),
408 + key: "Healthcheck"
409 + },
410 + {
411 + label: () =>
412 + h(
413 + RouterLink,
414 + {
415 + to: {
416 + name: "Metrics"
417 + }
418 + },
419 + { default: () => "Metrics Overview" }
420 + ),
421 + key: "Metrics"
422 + }
423 + ]
424 + },
425 + {
426 + label: "Indices",
427 + key: "IndicesMenu",
428 + icon: renderIcon(IndiciesIcon),
429 + children: [
430 + {
431 + label: () =>
432 + h(
433 + RouterLink,
434 + {
435 + to: {
436 + name: "Indices"
437 + }
438 + },
439 + { default: () => "Index Management" }
440 + ),
441 + key: "Indices"
442 + },
443 + {
444 + label: () =>
445 + h(
446 + RouterLink,
447 + {
448 + to: {
449 + name: "Snapshots"
450 + }
451 + },
452 + { default: () => "Snapshot & Restore" }
453 + ),
454 + key: "Snapshots"
455 + }
456 + ]
457 + },
458 + {
459 + label: "Graylog",
460 + key: "Graylog",
461 + icon: renderIcon(GraylogIcon),
462 + children: [
463 + {
464 + label: () =>
465 + h(
466 + RouterLink,
467 + {
468 + to: {
469 + name: "Graylog-Management"
470 + }
471 + },
472 + { default: () => "Management" }
473 + ),
474 + key: "Graylog-Management"
475 + },
476 + {
477 + label: () =>
478 + h(
479 + RouterLink,
480 + {
481 + to: {
482 + name: "Graylog-Metrics"
483 + }
484 + },
485 + { default: () => "Metrics" }
486 + ),
487 + key: "Graylog-Metrics"
488 + },
489 + {
490 + label: () =>
491 + h(
492 + RouterLink,
493 + {
494 + to: {
495 + name: "Graylog-Pipelines"
496 + }
497 + },
498 + { default: () => "Pipelines" }
499 + ),
500 + key: "Graylog-Pipelines"
501 + }
502 + ]
503 + },
504 + {
505 + label: () =>
506 + h(
507 + RouterLink,
508 + {
509 + to: {
510 + name: "Connectors"
511 + }
512 + },
513 + { default: () => "Connectors" }
514 + ),
515 + key: "Connectors",
516 + icon: renderIcon(ConnectorsIcon)
517 + },
518 + {
519 + label: "External Services",
520 + key: "ExternalServices",
521 + icon: renderIcon(ExternalServicesIcon),
522 + children: [
523 + {
524 + label: () =>
525 + h(
526 + RouterLink,
527 + {
528 + to: {
529 + name: "ExternalServices-ThirdPartyIntegrations"
530 + }
531 + },
532 + { default: () => "3rd Party Integrations" }
533 + ),
534 + key: "ExternalServices-ThirdPartyIntegrations"
535 + },
536 + {
537 + label: () =>
538 + h(
539 + RouterLink,
540 + {
541 + to: {
542 + name: "ExternalServices-NetworkConnectors"
543 + }
544 + },
545 + { default: () => "Network Connectors" }
546 + ),
547 + key: "ExternalServices-NetworkConnectors"
548 + },
549 + {
550 + label: () =>
551 + h(
552 + RouterLink,
553 + {
554 + to: {
555 + name: "ExternalServices-SingulAppAuth"
556 + }
557 + },
558 + { default: () => "Singul App Auth" }
559 + ),
560 + key: "ExternalServices-SingulAppAuth"
561 + }
562 + ]
563 + },
564 + {
565 + label: "Tools",
566 + key: "Tools",
567 + icon: renderIcon(ToolsIcon),
568 + children: [
569 + {
570 + label: "Stack Provisioning",
571 + key: "Tools-StackProvisioning"
572 + },
573 + {
574 + label: () =>
575 + h(
576 + RouterLink,
577 + {
578 + to: {
579 + name: "CloudSecurityAssessment"
580 + }
581 + },
582 + { default: () => "Cloud Security Assessment" }
583 + ),
584 + key: "CloudSecurityAssessment"
585 + },
586 + {
587 + label: () =>
588 + h(
589 + RouterLink,
590 + {
591 + to: {
592 + name: "WebVulnerabilityAssessment"
593 + }
594 + },
595 + { default: () => "Web Vulnerability Assessment" }
596 + ),
597 + key: "WebVulnerabilityAssessment"
598 + },
599 + {
600 + label: () =>
601 + h(
602 + RouterLink,
603 + {
604 + to: {
605 + name: "GitHubAudit"
606 + }
607 + },
608 + { default: () => "GitHub Audit" }
609 + ),
610 + key: "GitHubAudit"
611 + },
612 + {
613 + label: "Active Response",
614 + key: "Tools-ActiveResponse"
615 + },
616 + {
617 + label: "Threat Intel",
618 + key: "Tools-ThreatIntel"
619 + }
620 + ]
621 + },
622 + {
623 + label: () =>
624 + h(
625 + RouterLink,
626 + {
627 + to: {
628 + name: "Scheduler"
629 + }
630 + },
631 + { default: () => "Scheduler" }
632 + ),
633 + key: "Scheduler",
634 + icon: renderIcon(SchedulerIcon)
635 + },
636 + {
637 + label: () =>
638 + h(
639 + RouterLink,
640 + {
641 + to: {
642 + name: "CustomerPortal"
643 + }
644 + },
645 + { default: () => "Customer Portal" }
646 + ),
647 + key: "CustomerPortal",
648 + icon: renderIcon(CustomerPortalIcon)
649 + }
650 + ]
651 +}
customer_portal/src/app-layouts/common/Provider.vue new
+55
@@ -0,0 +1,55 @@
1 +<template>
2 + <n-config-provider
3 + :rtl="rtlOptions"
4 + :theme
5 + :theme-overrides
6 + :locale="providerLocale"
7 + :date-locale="providerDateLocale"
8 + preflight-style-disabled
9 + inline-theme-disabled
10 + >
11 + <n-loading-bar-provider container-class="h-0.75!">
12 + <n-message-provider>
13 + <n-notification-provider>
14 + <n-dialog-provider>
15 + <GlobalListener>
16 + <slot />
17 + </GlobalListener>
18 + </n-dialog-provider>
19 + </n-notification-provider>
20 + </n-message-provider>
21 + </n-loading-bar-provider>
22 + <n-global-style />
23 + </n-config-provider>
24 +</template>
25 +
26 +<script lang="ts" setup>
27 +import type { GlobalThemeOverrides } from "naive-ui"
28 +import type { RtlItem } from "naive-ui/es/config-provider/src/internal-interface"
29 +import {
30 + NConfigProvider,
31 + NDialogProvider,
32 + NGlobalStyle,
33 + NLoadingBarProvider,
34 + NMessageProvider,
35 + NNotificationProvider
36 +} from "naive-ui"
37 +import { computed, onBeforeMount } from "vue"
38 +import GlobalListener from "@/app-layouts/common/GlobalListener.vue"
39 +import { useLocalesStore } from "@/stores/i18n"
40 +import { useThemeStore } from "@/stores/theme"
41 +import { rtlStyles } from "./rtlProvider"
42 +
43 +const localesStore = useLocalesStore()
44 +const themeStore = useThemeStore()
45 +const theme = computed(() => themeStore.naiveTheme)
46 +const themeOverrides = computed<GlobalThemeOverrides>(() => themeStore.themeOverrides)
47 +const isRTL = computed(() => themeStore.isRTL)
48 +const rtlOptions = computed<RtlItem[] | undefined>(() => (isRTL.value ? rtlStyles : undefined))
49 +const providerLocale = computed(() => localesStore.naiveuiLocale)
50 +const providerDateLocale = computed(() => localesStore.naiveuiDateLocale)
51 +
52 +onBeforeMount(() => {
53 + themeStore.initTheme()
54 +})
55 +</script>
customer_portal/src/app-layouts/common/SplashScreen.vue new
+44
@@ -0,0 +1,44 @@
1 +<template>
2 + <transition name="loading-fade">
3 + <div v-if="show" class="splash-screen-loading-box">
4 + <n-spin :size="80" />
5 + </div>
6 + </transition>
7 +</template>
8 +
9 +<script lang="ts" setup>
10 +import { NSpin } from "naive-ui"
11 +
12 +const { show } = defineProps<{
13 + show: boolean
14 +}>()
15 +</script>
16 +
17 +<style scoped lang="scss">
18 +.splash-screen-loading-box {
19 + position: fixed;
20 + z-index: 99999;
21 + top: 0;
22 + left: 0;
23 + right: 0;
24 + bottom: 0;
25 + width: 100vw;
26 + height: 100vh;
27 + background: rgba(255, 255, 255, 0.2);
28 + backdrop-filter: blur(20px);
29 + display: flex;
30 + align-content: center;
31 + justify-content: center;
32 + align-items: center;
33 +
34 + &.loading-fade-enter-active,
35 + &.loading-fade-leave-active {
36 + transition: opacity 0.5s ease;
37 + }
38 +
39 + &.loading-fade-enter-from,
40 + &.loading-fade-leave-to {
41 + opacity: 0;
42 + }
43 +}
44 +</style>
customer_portal/src/app-layouts/common/Toolbar/Avatar.vue new
+88
@@ -0,0 +1,88 @@
1 +<template>
2 + <n-dropdown :options placement="bottom-end" @select="handleSelect">
3 + <n-avatar round :size="32" :src="userPic" :img-props="{ alt: 'avatar' }" />
4 + </n-dropdown>
5 +</template>
6 +
7 +<script lang="ts" setup>
8 +import { NAvatar, NDropdown } from "naive-ui"
9 +import { h, ref } from "vue"
10 +import { useRouter } from "vue-router"
11 +import { useAuthStore } from "@/stores/auth"
12 +import { renderIcon } from "@/utils"
13 +
14 +const UserIcon = "ion:person-outline"
15 +const LicenseIcon = "carbon:license"
16 +const LogoutIcon = "ion:log-out-outline"
17 +const LogsIcon = "carbon:cloud-logging"
18 +const ContactIcon = "ic:outline-alternate-email"
19 +const DocsIcon = "carbon:document"
20 +const UsersIcon = "carbon:group-security"
21 +
22 +const router = useRouter()
23 +const authStore = useAuthStore()
24 +
25 +const userPic = authStore.userPic
26 +
27 +const options = ref([
28 + {
29 + label: "Profile",
30 + key: "route-Profile",
31 + icon: renderIcon(UserIcon)
32 + },
33 + {
34 + label: "License",
35 + key: "route-License",
36 + icon: renderIcon(LicenseIcon)
37 + },
38 + {
39 + label: "Users",
40 + key: "route-Users",
41 + icon: renderIcon(UsersIcon)
42 + },
43 + {
44 + label: "Logs",
45 + key: "route-Logs",
46 + icon: renderIcon(LogsIcon)
47 + },
48 + {
49 + label: () =>
50 + h(
51 + "a",
52 + {
53 + href: "https://docs.socfortress.co/",
54 + target: "_blank",
55 + rel: "noopener noreferrer"
56 + },
57 + "Documentation"
58 + ),
59 + key: "documentation",
60 + icon: renderIcon(DocsIcon)
61 + },
62 + {
63 + label: () =>
64 + h(
65 + "a",
66 + {
67 + href: "https://www.socfortress.co/contact-us",
68 + target: "_blank",
69 + rel: "noopener noreferrer"
70 + },
71 + "Contact SOCFortress"
72 + ),
73 + key: "contact-socfortress",
74 + icon: renderIcon(ContactIcon)
75 + },
76 + {
77 + label: "Logout",
78 + key: "route-Logout",
79 + icon: renderIcon(LogoutIcon)
80 + }
81 +])
82 +function handleSelect(key: string) {
83 + if (key.indexOf("route-") === 0) {
84 + const path = key.split("route-")[1]
85 + router.push({ name: path })
86 + }
87 +}
88 +</script>
customer_portal/src/app-layouts/common/Toolbar/Breadcrumb.vue new
+109
@@ -0,0 +1,109 @@
1 +<template>
2 + <n-breadcrumb class="breadcrumb">
3 + <n-breadcrumb-item @click="goto({ path: '/' })">
4 + <Icon :size="16" :name="HomeIcon" />
5 + </n-breadcrumb-item>
6 + <TransitionGroup name="anim">
7 + <n-breadcrumb-item
8 + v-for="(item, index) of items"
9 + :key="item.key"
10 + :class="`index-${index}`"
11 + @click="goto({ path: item.path })"
12 + >
13 + {{ item.name }}
14 + </n-breadcrumb-item>
15 + </TransitionGroup>
16 + </n-breadcrumb>
17 +</template>
18 +
19 +<script lang="ts" setup>
20 +import type { RouteLocationNormalizedLoaded } from "vue-router"
21 +import type { BreadcrumbItem } from "@/composables/useBreadcrumb"
22 +import _capitalize from "lodash/capitalize"
23 +import _compact from "lodash/compact"
24 +import _isEqual from "lodash/isEqual"
25 +import _split from "lodash/split"
26 +import { NBreadcrumb, NBreadcrumbItem } from "naive-ui"
27 +import { onBeforeMount } from "vue"
28 +import { useRoute, useRouter } from "vue-router"
29 +import Icon from "@/components/common/Icon.vue"
30 +import { useBreadcrumb } from "@/composables/useBreadcrumb"
31 +
32 +const HomeIcon = "fluent:home-24-regular"
33 +const router = useRouter()
34 +const route = useRoute()
35 +const { items, setItems } = useBreadcrumb()
36 +
37 +function goto(page: Partial<BreadcrumbItem>) {
38 + if (page.name && page.name !== route.name) {
39 + router.push({ name: page.name })
40 + }
41 + if (page.path && page.path !== route.path) {
42 + router.push({ path: page.path })
43 + }
44 +}
45 +
46 +function checkRoute(route: RouteLocationNormalizedLoaded) {
47 + const newItems: BreadcrumbItem[] = []
48 + let pathChunks = _compact(_split(route?.path || "", "/"))
49 + if (!pathChunks.length) {
50 + pathChunks = _compact(_split(route?.matched?.[0]?.aliasOf?.path || "", "/"))
51 + }
52 +
53 + let cumulativePath = ""
54 +
55 + for (const chunk of pathChunks) {
56 + const name = _capitalize(chunk)
57 + cumulativePath += `/${chunk}`
58 +
59 + newItems.push({
60 + name,
61 + path: cumulativePath,
62 + key: name + cumulativePath
63 + })
64 + }
65 +
66 + if (route.meta?.title && newItems.length) {
67 + const lastItem = newItems.at(-1)
68 + if (lastItem) {
69 + lastItem.name = route.meta.title
70 + }
71 + }
72 +
73 + if (!_isEqual(items.value, newItems)) {
74 + setItems(newItems)
75 + }
76 +}
77 +
78 +onBeforeMount(() => {
79 + checkRoute(router.currentRoute.value)
80 +
81 + router.beforeResolve(route => {
82 + checkRoute(route)
83 + })
84 +})
85 +</script>
86 +
87 +<style lang="scss" scoped>
88 +.breadcrumb {
89 + .anim-move,
90 + .anim-enter-active {
91 + transition: all 0.5s var(--bezier-ease);
92 +
93 + @for $i from 0 through 10 {
94 + &.index-#{$i} {
95 + transition-delay: $i * 0.1s;
96 + }
97 + }
98 + }
99 +
100 + .anim-leave-active {
101 + display: none;
102 + }
103 +
104 + .anim-enter-from {
105 + opacity: 0;
106 + transform: translateX(-5px);
107 + }
108 +}
109 +</style>
customer_portal/src/app-layouts/common/Toolbar/FullscreenSwitch.vue new
+20
@@ -0,0 +1,20 @@
1 +<template>
2 + <button
3 + class="hidden cursor-pointer items-center justify-center lg:flex"
4 + alt="fullscreen-switch"
5 + aria-label="fullscreen-switch"
6 + @click="toggle()"
7 + >
8 + <Icon v-if="isFullscreen" :size="20" :name="CloseIcon" />
9 + <Icon v-else :size="20" :name="OpenIcon" />
10 + </button>
11 +</template>
12 +
13 +<script lang="ts" setup>
14 +import Icon from "@/components/common/Icon.vue"
15 +import { useFullscreenSwitch } from "@/composables/useFullscreenSwitch"
16 +
17 +const { isFullscreen, toggle } = useFullscreenSwitch()
18 +const OpenIcon = "fluent:full-screen-maximize-24-regular"
19 +const CloseIcon = "fluent:full-screen-minimize-24-regular"
20 +</script>
customer_portal/src/app-layouts/common/Toolbar/LocaleSwitch.vue new
+52
@@ -0,0 +1,52 @@
1 +<template>
2 + <n-popselect v-model:value="currentLocale" :options="list" :render-label>
3 + <Icon :size="19" :name="MultiLanguageIcon" />
4 + </n-popselect>
5 +</template>
6 +
7 +<script lang="ts" setup>
8 +import type { SelectOption } from "naive-ui"
9 +import type { VNodeChild } from "vue"
10 +import { NPopselect } from "naive-ui"
11 +import { computed, h } from "vue"
12 +import { useI18n } from "vue-i18n"
13 +import Icon from "@/components/common/Icon.vue"
14 +import { useLocalesStore } from "@/stores/i18n"
15 +
16 +const MultiLanguageIcon = "ion:language-outline"
17 +const localesStore = useLocalesStore()
18 +const { setLocale } = localesStore
19 +const { t } = useI18n()
20 +
21 +const list = computed(() =>
22 + localesStore.availableLocales.map(i => ({
23 + label: i,
24 + value: i
25 + }))
26 +)
27 +
28 +const currentLocale = computed({
29 + get: () => localesStore.locale,
30 + set: v => setLocale(v)
31 +})
32 +
33 +function renderLabel(option: SelectOption): VNodeChild {
34 + return [
35 + h(Icon, {
36 + color: "#000",
37 + style: {
38 + verticalAlign: "-0.15em",
39 + marginRight: "8px"
40 + },
41 + name: `circle-flags:${option.label}`
42 + }),
43 + h(
44 + "span",
45 + {},
46 + {
47 + default: () => t(`locales.${option.label}`, `${option.label}`)
48 + }
49 + )
50 + ]
51 +}
52 +</script>
customer_portal/src/app-layouts/common/Toolbar/Notifications.vue new
+56
@@ -0,0 +1,56 @@
1 +<template>
2 + <n-popover :show-arrow="false" placement="bottom" content-class="w-72 p-0!">
3 + <template #trigger>
4 + <n-badge :show="hasUnread" dot :color="primaryColor">
5 + <Icon :name="BellIcon" :size="21" class="text-default" />
6 + </n-badge>
7 + </template>
8 + <template #header>
9 + <div class="font-semibold">Notifications</div>
10 + </template>
11 + <template #default>
12 + <NotificationsList :max-items="MAX_ITEMS" class="max-h-[50vh]">
13 + <template #last>
14 + <div v-if="list.length > MAX_ITEMS" class="flex justify-center p-4">
15 + <n-button text @click="showDrawer = true">View all</n-button>
16 + </div>
17 + </template>
18 + </NotificationsList>
19 + </template>
20 + <template #footer>
21 + <NotificationsToolbar />
22 + </template>
23 + </n-popover>
24 +
25 + <n-drawer v-model:show="showDrawer" :width="400" class="max-w-[90vw]" :trap-focus="false">
26 + <n-drawer-content title="Notifications" closable body-content-class="p-0!">
27 + <NotificationsList />
28 + <template #footer>
29 + <NotificationsToolbar />
30 + </template>
31 + </n-drawer-content>
32 + </n-drawer>
33 +</template>
34 +
35 +<script lang="ts" setup>
36 +import { NBadge, NButton, NDrawer, NDrawerContent, NPopover } from "naive-ui"
37 +import { computed, onBeforeMount, ref } from "vue"
38 +import Icon from "@/components/common/Icon.vue"
39 +import NotificationsList from "@/components/common/Notifications/List.vue"
40 +import NotificationsToolbar from "@/components/common/Notifications/Toolbar.vue"
41 +import { useHealthchecksNotify } from "@/composables/useHealthchecksNotify"
42 +import { useNotifications } from "@/composables/useNotifications"
43 +import { useThemeStore } from "@/stores/theme"
44 +
45 +const MAX_ITEMS = 7
46 +const BellIcon = "ph:bell"
47 +const themeStore = useThemeStore()
48 +const primaryColor = computed(() => themeStore.style["primary-color"])
49 +const hasUnread = useNotifications().hasUnread
50 +const showDrawer = ref(false)
51 +const list = useNotifications().list
52 +
53 +onBeforeMount(() => {
54 + useHealthchecksNotify().init()
55 +})
56 +</script>
customer_portal/src/app-layouts/common/Toolbar/PillWrapper.vue new
+17
@@ -0,0 +1,17 @@
1 +<template>
2 + <div class="pill-wrapper flex items-center">
3 + <slot />
4 + </div>
5 +</template>
6 +
7 +<style lang="scss" scoped>
8 +.pill-wrapper {
9 + background-color: var(--bg-sidebar-color);
10 + border: 1px solid var(--border-color);
11 + color: var(--fg-default-color);
12 + border-radius: 50px;
13 + padding: 6px;
14 + transition: all 0.3s;
15 + gap: 14px;
16 +}
17 +</style>
customer_portal/src/app-layouts/common/Toolbar/PinnedPagesV2.vue new
+276
@@ -0,0 +1,276 @@
1 +<template>
2 + <div class="pinned-pages flex items-center gap-5">
3 + <TransitionGroup name="anim" tag="div" class="latest-list flex items-center gap-4 overflow-hidden">
4 + <n-tag
5 + v-for="page of latestSanitized"
6 + :key="page.name"
7 + round
8 + :bordered="false"
9 + :closable="false"
10 + @close="removeLatestPage(page.name)"
11 + >
12 + <span class="page-name" :title="page.title" @click="gotoPage(page.name)">
13 + {{ page.title }}
14 + </span>
15 + <template #icon>
16 + <div class="icon-box" @click="pinPage(page)">
17 + <Icon :size="14" :name="PinnedIcon" />
18 + </div>
19 + </template>
20 + </n-tag>
21 + </TransitionGroup>
22 +
23 + <Transition name="anim" tag="div" class="shortcuts-container flex items-center">
24 + <div v-if="pinned.length" class="flex items-center">
25 + <n-popover :show-arrow="false" placement="bottom" trigger="hover" class="p-1!">
26 + <template #trigger>
27 + <button class="shortcuts-btn flex items-center gap-2 text-sm">
28 + <span>Shortcuts</span>
29 + <n-badge :value="pinned.length" />
30 + </button>
31 + </template>
32 + <div class="flex flex-col">
33 + <n-button
34 + v-for="page of pinned"
35 + :key="page.name"
36 + size="small"
37 + quaternary
38 + class="justify-start!"
39 + @click="gotoPage(page.name)"
40 + >
41 + <span class="flex items-center gap-1">
42 + <Icon
43 + :size="20"
44 + :name="CloseIcon"
45 + class="opacity-50 hover:text-red-500 hover:opacity-100"
46 + @click.stop="removePinnedPage(page.name)"
47 + />
48 + <span class="px-2">
49 + {{ page.title }}
50 + </span>
51 + </span>
52 + </n-button>
53 + </div>
54 + </n-popover>
55 + </div>
56 + </Transition>
57 + </div>
58 +</template>
59 +
60 +<script lang="ts" setup>
61 +import type { RemovableRef } from "@vueuse/core"
62 +import type { ComputedRef } from "vue"
63 +import type { RouteLocationNormalized, RouteRecordName } from "vue-router"
64 +import { useStorage } from "@vueuse/core"
65 +import _split from "lodash/split"
66 +import _takeRight from "lodash/takeRight"
67 +import _uniqBy from "lodash/uniqBy"
68 +import { NBadge, NButton, NPopover, NTag } from "naive-ui"
69 +import { computed } from "vue"
70 +import { useRouter } from "vue-router"
71 +import Icon from "@/components/common/Icon.vue"
72 +
73 +interface Page {
74 + name: RouteRecordName | string
75 + fullPath: string
76 + title: string
77 +}
78 +
79 +const PinnedIcon = "tabler:pinned"
80 +const CloseIcon = "carbon:close"
81 +const router = useRouter()
82 +const latest: RemovableRef<Page[]> = useStorage<Page[]>("latest-pages", [], sessionStorage)
83 +const pinned: RemovableRef<Page[]> = useStorage<Page[]>("pinned-pages", [], localStorage)
84 +const latestSanitized: ComputedRef<Page[]> = computed(() => {
85 + return _takeRight(
86 + latest.value.filter(page => pinned.value.findIndex(p => p.name === page.name) === -1).reverse(),
87 + 3
88 + ) as Page[]
89 +})
90 +
91 +function removeLatestPage(pageName: RouteRecordName | string) {
92 + latest.value = latest.value.filter(page => page.name !== pageName)
93 + return true
94 +}
95 +function removePinnedPage(pageName: RouteRecordName | string) {
96 + pinned.value = pinned.value.filter(page => page.name !== pageName)
97 + return true
98 +}
99 +function gotoPage(pageName: RouteRecordName | string) {
100 + router.push({ name: pageName })
101 + return true
102 +}
103 +
104 +function pinPage(page: Page) {
105 + const isPresent = pinned.value.findIndex(p => p.name === page.name) !== -1
106 + if (!isPresent) {
107 + pinned.value = [page, ...pinned.value]
108 + }
109 + return true
110 +}
111 +
112 +function checkRoute(route: RouteLocationNormalized) {
113 + const splitName = _split(route.name?.toString(), "-")
114 + const title = route.meta?.title || splitName.at(-1)
115 +
116 + if (route.name && title && !route.meta?.skipPin) {
117 + const page: Page = {
118 + name: route.name,
119 + fullPath: route.fullPath,
120 + title
121 + }
122 + latest.value = _uniqBy([page, ...latest.value, page], "name")
123 + }
124 +}
125 +
126 +router.afterEach(route => {
127 + checkRoute(route)
128 +})
129 +</script>
130 +
131 +<style lang="scss" scoped>
132 +.pinned-pages {
133 + position: relative;
134 + overflow: hidden;
135 + padding: 8px 0;
136 + container-type: inline-size;
137 + justify-content: flex-end;
138 + width: 100%;
139 +
140 + :deep() {
141 + .n-tag {
142 + background-color: transparent;
143 + flex-shrink: 1;
144 + overflow: hidden;
145 + gap: 4px;
146 +
147 + &.n-tag--round {
148 + padding: 0 !important;
149 + transition: all 0.3s;
150 + }
151 + .n-tag__icon {
152 + margin: 0 !important;
153 + }
154 + .n-tag__content {
155 + overflow: hidden;
156 + text-overflow: ellipsis;
157 + }
158 + .n-tag__close {
159 + overflow: hidden;
160 + width: 0px;
161 + margin-left: 0;
162 + margin-right: 0;
163 + transition: all 0.3s;
164 + }
165 +
166 + &.n-tag--closable {
167 + &:hover {
168 + background-color: var(--bg-sidebar-color);
169 +
170 + &.n-tag--round {
171 + padding: 0 calc(var(--n-height) / 3.6) 0 calc(var(--n-height) / 3.6);
172 + }
173 + .n-tag__close {
174 + margin-left: 5px;
175 + overflow: initial;
176 + width: 14px;
177 + }
178 + }
179 + }
180 + }
181 + }
182 +
183 + .shortcuts-btn {
184 + border-radius: 50px;
185 + background-color: var(--bg-sidebar-color);
186 + gap: 10px;
187 + height: 32px;
188 + cursor: pointer;
189 + padding: 0px 6px;
190 + padding-left: 12px;
191 + outline: none;
192 + transition: all 0.2s var(--bezier-ease);
193 + border: 1px solid var(--border-color);
194 +
195 + &:hover {
196 + background-color: var(--hover-color);
197 + }
198 +
199 + :deep() {
200 + .n-badge {
201 + .n-badge-sup {
202 + --size: 22px;
203 + background-color: var(--bg-body-color);
204 + color: var(--fg-secondary-color);
205 + font-weight: 700;
206 + height: var(--size);
207 + line-height: var(--size);
208 + min-width: var(--size);
209 + border-radius: var(--size);
210 + font-variant-numeric: tabular-nums;
211 + justify-content: center;
212 + padding: 0;
213 + }
214 + }
215 + }
216 + }
217 +
218 + .page-name {
219 + cursor: pointer;
220 + overflow: hidden;
221 + text-overflow: ellipsis;
222 +
223 + &:hover {
224 + text-decoration: underline;
225 + text-decoration-thickness: 2px;
226 + text-decoration-color: var(--primary-color);
227 + }
228 + }
229 +
230 + .icon-box {
231 + cursor: pointer;
232 + transition: color 0.3s;
233 +
234 + &:hover {
235 + color: var(--primary-color);
236 + }
237 + }
238 +
239 + .anim-move,
240 + .anim-enter-active,
241 + .anim-leave-active {
242 + transition: all 0.5s var(--bezier-ease);
243 + }
244 +
245 + .anim-enter-from,
246 + .anim-leave-to {
247 + opacity: 0;
248 + transform: scale(0);
249 + }
250 +
251 + .anim-leave-active {
252 + position: absolute;
253 + }
254 +
255 + @container (max-width: 460px) {
256 + .latest-list {
257 + display: none;
258 + }
259 + }
260 +
261 + @container (max-width: 140px) {
262 + .shortcuts-container {
263 + display: none;
264 + }
265 + }
266 +}
267 +
268 +.direction-rtl {
269 + .pinned-pages {
270 + .shortcuts-btn {
271 + padding-right: 12px;
272 + padding-left: 6px;
273 + }
274 + }
275 +}
276 +</style>
customer_portal/src/app-layouts/common/Toolbar/Search.vue new
+128
@@ -0,0 +1,128 @@
1 +<template>
2 + <button class="search-btn flex items-center" @click="openBox">
3 + <Icon :name="SearchIcon" :size="16" class="search-btn-icon" />
4 + <span>Search</span>
5 + <n-text code class="search-command">
6 + <span :class="{ win: commandIcon === 'CTRL' }">{{ commandIcon }}</span>
7 + K
8 + </n-text>
9 + </button>
10 +</template>
11 +
12 +<script lang="ts" setup>
13 +import { NText } from "naive-ui"
14 +import { onMounted, ref } from "vue"
15 +import Icon from "@/components/common/Icon.vue"
16 +import { useSearchDialog } from "@/composables/useSearchDialog"
17 +import { getNavigatorOS } from "@/utils"
18 +
19 +const SearchIcon = "ion:search-outline"
20 +const commandIcon = ref("⌘")
21 +
22 +function openBox() {
23 + useSearchDialog().open()
24 +}
25 +
26 +onMounted(() => {
27 + const isWindows = getNavigatorOS() === "Windows"
28 + commandIcon.value = isWindows ? "CTRL" : "⌘"
29 +})
30 +</script>
31 +
32 +<style lang="scss" scoped>
33 +.search-btn {
34 + border-radius: 50px;
35 + background-color: var(--bg-body-color);
36 + gap: 10px;
37 + height: 32px;
38 + cursor: pointer;
39 + padding: 4px 10px;
40 + padding-right: 6px;
41 + outline: none;
42 + border: none;
43 +
44 + .search-command {
45 + white-space: nowrap;
46 +
47 + span {
48 + line-height: 0;
49 + position: relative;
50 + top: 1px;
51 + font-size: 16px;
52 +
53 + &.win {
54 + font-size: inherit;
55 + top: 0;
56 + }
57 + }
58 + }
59 +
60 + :deep() {
61 + & > .n-icon {
62 + opacity: 0.5;
63 + transition: opacity 0.3s;
64 + }
65 + }
66 + & > span {
67 + opacity: 0.5;
68 + padding-right: 2px;
69 + font-size: 14px;
70 + transition: opacity 0.3s;
71 + }
72 +
73 + :deep() {
74 + & > code {
75 + background-color: var(--bg-sidebar-color);
76 + border-top-right-radius: 10px;
77 + border-bottom-right-radius: 10px;
78 + padding-right: 10px;
79 + }
80 + }
81 +
82 + &:hover {
83 + :deep() {
84 + & > .n-icon {
85 + opacity: 0.9;
86 + }
87 + }
88 + & > span {
89 + opacity: 0.9;
90 + }
91 + }
92 +
93 + @media (max-width: 1000px) {
94 + padding-right: 8px !important;
95 + padding-left: 8px !important;
96 +
97 + & > span {
98 + display: none;
99 + }
100 + & > .n-text--code {
101 + display: none;
102 + }
103 + }
104 +}
105 +
106 +.direction-rtl {
107 + .search-btn {
108 + padding-right: 10px;
109 + padding-left: 6px;
110 +
111 + .search-btn-icon {
112 + transform: rotateY(180deg);
113 + }
114 +
115 + :deep() {
116 + & > code {
117 + border-top-left-radius: 10px;
118 + border-bottom-left-radius: 10px;
119 + padding-left: 10px;
120 +
121 + border-top-right-radius: var(--n-code-border-radius);
122 + border-bottom-right-radius: var(--n-code-border-radius);
123 + padding-right: 0.35em;
124 + }
125 + }
126 + }
127 +}
128 +</style>
customer_portal/src/app-layouts/common/Toolbar/ThemeSwitch.vue new
+123
@@ -0,0 +1,123 @@
1 +<template>
2 + <button class="theme-switch" alt="theme-switch" aria-label="theme-switch" @click="toggleTheme">
3 + <Transition name="rotate">
4 + <Icon v-if="isThemeDark" :size="20">
5 + <Iconify :icon="Sunny" class="hover" />
6 + <Iconify :icon="SunnyOutline" />
7 + </Icon>
8 + <Icon v-else :size="20">
9 + <Iconify :icon="Moon" class="hover" />
10 + <Iconify :icon="MoonOutline" />
11 + </Icon>
12 + </Transition>
13 + </button>
14 +</template>
15 +
16 +<script lang="ts" setup>
17 +import { Icon as Iconify } from "@iconify/vue"
18 +import { computed, nextTick } from "vue"
19 +import Icon from "@/components/common/Icon.vue"
20 +import { useThemeStore } from "@/stores/theme"
21 +
22 +const Sunny = "ion:sunny"
23 +const Moon = "ion:moon"
24 +const SunnyOutline = "ion:sunny-outline"
25 +const MoonOutline = "ion:moon-outline"
26 +const themeStore = useThemeStore()
27 +const isThemeDark = computed<boolean>(() => themeStore.isThemeDark)
28 +
29 +function toggleTheme(event?: MouseEvent) {
30 + const isAppearanceTransition =
31 + typeof document !== "undefined" &&
32 + document.startViewTransition &&
33 + !window.matchMedia("(prefers-reduced-motion: reduce)").matches
34 +
35 + if (!isAppearanceTransition || !event) {
36 + themeStore.toggleTheme()
37 + return
38 + }
39 +
40 + if (document?.startViewTransition) {
41 + const x = event.clientX ?? innerWidth / 2
42 + const y = event.clientY ?? innerHeight / 2
43 + const endRadius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y))
44 +
45 + const transition = document.startViewTransition(async () => {
46 + themeStore.toggleTheme()
47 + await nextTick()
48 + })
49 +
50 + transition.ready.then(() => {
51 + const clipPath = [`circle(0px at ${x}px ${y}px)`, `circle(${endRadius}px at ${x}px ${y}px)`]
52 + // const clipPath = [`inset(50%)`, `inset(0)`]
53 +
54 + document.documentElement.animate(
55 + {
56 + clipPath
57 + },
58 + {
59 + duration: 300,
60 + easing: "ease-in",
61 + pseudoElement: "::view-transition-new(root)"
62 + }
63 + )
64 + })
65 + }
66 +}
67 +</script>
68 +
69 +<style scoped lang="scss">
70 +.theme-switch {
71 + position: relative;
72 + width: 20px;
73 + height: 20px;
74 + overflow: hidden;
75 + outline: none;
76 + border: none;
77 +
78 + :deep() {
79 + .n-icon {
80 + position: absolute;
81 + top: 0;
82 + left: 0;
83 +
84 + & > svg {
85 + position: absolute;
86 + top: 0;
87 + left: 0;
88 + transition: opacity 0.35s;
89 +
90 + &.hover {
91 + opacity: 0;
92 + }
93 + &:not(.hover) {
94 + opacity: 1;
95 + }
96 + }
97 +
98 + &:hover {
99 + & > svg {
100 + &.hover {
101 + opacity: 1;
102 + }
103 + &:not(.hover) {
104 + opacity: 0;
105 + }
106 + }
107 + }
108 + }
109 + }
110 +}
111 +.rotate-enter-active,
112 +.rotate-leave-active {
113 + transition: all 0.5s ease-out;
114 +}
115 +.rotate-enter-from {
116 + opacity: 0;
117 + transform: rotate(45deg);
118 +}
119 +.rotate-leave-to {
120 + opacity: 0;
121 + transform: rotate(-45deg);
122 +}
123 +</style>
customer_portal/src/app-layouts/common/Toolbar/Toolbar.vue new
+150
@@ -0,0 +1,150 @@
1 +<template>
2 + <header class="toolbar" :class="{ boxed }">
3 + <div class="wrap flex items-center justify-end gap-3">
4 + <div class="logo-box flex cursor-pointer items-center gap-2" @click="openNav()">
5 + <Logo type="small" />
6 + <Icon :size="20" name="carbon:chevron-right" />
7 + </div>
8 +
9 + <Breadcrumb class="grow" />
10 + <PinnedPages />
11 +
12 + <PillWrapper>
13 + <Search />
14 + <FullscreenSwitch />
15 + <ThemeSwitch />
16 + <Notifications />
17 + <Avatar />
18 + </PillWrapper>
19 + </div>
20 +
21 + <BlurEffect />
22 + <div v-if="gradient" :class="`gradient-${gradient}`"></div>
23 + </header>
24 +</template>
25 +
26 +<script lang="ts" setup>
27 +import BlurEffect from "@/components/common/BlurEffect.vue"
28 +import Icon from "@/components/common/Icon.vue"
29 +import { useThemeStore } from "@/stores/theme"
30 +import Logo from "../Logo.vue"
31 +import Avatar from "./Avatar.vue"
32 +import Breadcrumb from "./Breadcrumb.vue"
33 +import FullscreenSwitch from "./FullscreenSwitch.vue"
34 +import Notifications from "./Notifications.vue"
35 +import PillWrapper from "./PillWrapper.vue"
36 +import PinnedPages from "./PinnedPagesV2.vue"
37 +import Search from "./Search.vue"
38 +import ThemeSwitch from "./ThemeSwitch.vue"
39 +
40 +const { boxed, gradient } = defineProps<{
41 + boxed: boolean
42 + gradient?: "body" | "sidebar"
43 +}>()
44 +
45 +const themeStore = useThemeStore()
46 +const openNav = () => themeStore.openSidebar()
47 +</script>
48 +
49 +<style lang="scss" scoped>
50 +.toolbar {
51 + position: sticky;
52 + top: 0;
53 + left: 0;
54 + height: var(--toolbar-height);
55 + width: 100%;
56 + max-width: 100%;
57 + padding: 0 var(--view-padding);
58 + z-index: 3;
59 + overflow: hidden;
60 +
61 + .wrap {
62 + height: var(--toolbar-height);
63 + overflow: hidden;
64 + width: 100%;
65 + max-width: 100%;
66 + position: relative;
67 + z-index: 1;
68 +
69 + @media (max-width: 850px) {
70 + .pinned-pages {
71 + display: none;
72 + }
73 + }
74 +
75 + @media (max-width: 700px) {
76 + justify-content: space-between;
77 + .breadcrumb {
78 + display: none;
79 + }
80 + }
81 + }
82 +
83 + &.boxed {
84 + padding: 0;
85 + .wrap {
86 + padding: 0 var(--view-padding);
87 + max-width: var(--boxed-width);
88 + margin: 0 auto;
89 + }
90 + }
91 +
92 + .gradient-sidebar {
93 + position: absolute;
94 + inset: 0;
95 + background-color: var(--bg-sidebar-color);
96 + background: linear-gradient(
97 + to bottom,
98 + rgba(var(--bg-sidebar-color-rgb) / 1) 0%,
99 + rgba(var(--bg-sidebar-color-rgb) / 0.945) 8.6%,
100 + rgba(var(--bg-sidebar-color-rgb) / 0.888) 16.2%,
101 + rgba(var(--bg-sidebar-color-rgb) / 0.83) 22.9%,
102 + rgba(var(--bg-sidebar-color-rgb) / 0.769) 28.9%,
103 + rgba(var(--bg-sidebar-color-rgb) / 0.707) 34.4%,
104 + rgba(var(--bg-sidebar-color-rgb) / 0.644) 39.5%,
105 + rgba(var(--bg-sidebar-color-rgb) / 0.578) 44.5%,
106 + rgba(var(--bg-sidebar-color-rgb) / 0.511) 49.5%,
107 + rgba(var(--bg-sidebar-color-rgb) / 0.443) 54.7%,
108 + rgba(var(--bg-sidebar-color-rgb) / 0.373) 60.3%,
109 + rgba(var(--bg-sidebar-color-rgb) / 0.301) 66.4%,
110 + rgba(var(--bg-sidebar-color-rgb) / 0.228) 73.3%,
111 + rgba(var(--bg-sidebar-color-rgb) / 0.153) 81%,
112 + rgba(var(--bg-sidebar-color-rgb) / 0.077) 89.9%,
113 + rgba(var(--bg-sidebar-color-rgb) / 0) 100%
114 + );
115 + }
116 + .gradient-body {
117 + position: absolute;
118 + inset: 0;
119 + background-color: var(--bg-body-color);
120 + background: linear-gradient(
121 + to bottom,
122 + rgba(var(--bg-body-color-rgb) / 1) 0%,
123 + rgba(var(--bg-body-color-rgb) / 0.738) 19%,
124 + rgba(var(--bg-body-color-rgb) / 0.541) 34%,
125 + rgba(var(--bg-body-color-rgb) / 0.382) 47%,
126 + rgba(var(--bg-body-color-rgb) / 0.278) 56.5%,
127 + rgba(var(--bg-body-color-rgb) / 0.194) 65%,
128 + rgba(var(--bg-body-color-rgb) / 0.126) 73%,
129 + rgba(var(--bg-body-color-rgb) / 0.075) 80.2%,
130 + rgba(var(--bg-body-color-rgb) / 0.042) 86.1%,
131 + rgba(var(--bg-body-color-rgb) / 0.021) 91%,
132 + rgba(var(--bg-body-color-rgb) / 0.008) 95.2%,
133 + rgba(var(--bg-body-color-rgb) / 0.002) 98.2%,
134 + rgba(var(--bg-body-color-rgb) / 0) 100%
135 + );
136 + }
137 +}
138 +
139 +.direction-rtl {
140 + .toolbar {
141 + .wrap {
142 + .logo-box {
143 + .n-icon {
144 + transform: rotateY(180deg);
145 + }
146 + }
147 + }
148 + }
149 +}
150 +</style>
customer_portal/src/app-layouts/common/Toolbar/index.ts new
+1
@@ -0,0 +1 @@
1 +export { default } from "./Toolbar.vue"
customer_portal/src/app-layouts/common/rtlProvider.ts new
+70
@@ -0,0 +1,70 @@
1 +import type { RtlItem } from "naive-ui/es/config-provider/src/internal-interface"
2 +import {
3 + unstableAlertRtl,
4 + unstableAvatarGroupRtl,
5 + unstableButtonGroupRtl,
6 + // unstableBadgeRtl,
7 + unstableButtonRtl,
8 + unstableCardRtl,
9 + unstableCheckboxRtl,
10 + unstableCollapseRtl,
11 + unstableCollapseTransitionRtl,
12 + unstableDataTableRtl,
13 + unstableDialogRtl,
14 + unstableDrawerRtl,
15 + unstableDynamicInputRtl,
16 + unstableFlexRtl,
17 + unstableInputNumberRtl,
18 + unstableInputRtl,
19 + unstableListRtl,
20 + unstableMessageRtl,
21 + unstableNotificationRtl,
22 + unstablePaginationRtl,
23 + unstableRadioRtl,
24 + unstableRowRtl,
25 + unstableScrollbarRtl,
26 + unstableSelectRtl,
27 + unstableSpaceRtl,
28 + unstableStatisticRtl,
29 + unstableStepsRtl,
30 + unstableTableRtl,
31 + unstableTagRtl,
32 + unstableThingRtl,
33 + unstableTreeRtl
34 +} from "naive-ui"
35 +
36 +const rtlStyles: RtlItem[] = [
37 + unstableAlertRtl,
38 + unstableAvatarGroupRtl,
39 + // unstableBadgeRtl,
40 + unstableButtonRtl,
41 + unstableNotificationRtl,
42 + unstableMessageRtl,
43 + unstableDataTableRtl,
44 + unstableButtonGroupRtl,
45 + unstableCardRtl,
46 + unstableCheckboxRtl,
47 + unstableCollapseRtl,
48 + unstableCollapseTransitionRtl,
49 + unstableDialogRtl,
50 + unstableDrawerRtl,
51 + unstableDynamicInputRtl,
52 + unstableFlexRtl,
53 + unstableInputRtl,
54 + unstableInputNumberRtl,
55 + unstableListRtl,
56 + unstablePaginationRtl,
57 + unstableRadioRtl,
58 + unstableSelectRtl,
59 + unstableSpaceRtl,
60 + unstableStatisticRtl,
61 + unstableStepsRtl,
62 + unstableTableRtl,
63 + unstableTagRtl,
64 + unstableThingRtl,
65 + unstableTreeRtl,
66 + unstableRowRtl,
67 + unstableScrollbarRtl
68 +]
69 +
70 +export { rtlStyles }
customer_portal/src/assets/icons/alert-settings-icon.svg new
+6
@@ -0,0 +1,6 @@
1 +<svg width="24" height="24" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
2 + <path id="Percorso" fill="none" stroke="currentColor" stroke-width="7" stroke-linejoin="round" d="M 68.567665 43.250656 L 50.283127 12.225441 C 49.26506 10.499969 46.749729 10.517128 45.754795 12.256355 L 2.492177 87.871727 C 1.497366 89.61084 2.769681 91.765846 4.78288 91.752052 L 48.279461 91.752052"/>
3 + <path id="path1" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round" stroke-linejoin="round" d="M 48.453129 74.678711 L 48.406059 74.679123"/>
4 + <path id="path2" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round" stroke-linejoin="round" d="M 48.279461 63.363575 L 48.279865 40.032139"/>
5 + <path id="path3" fill="currentColor" fill-rule="evenodd" stroke="none" d="M 74.265823 51.852451 C 73.775658 52.338959 73.775658 53.121677 73.775658 54.687527 L 73.775658 56.174652 C 73.775658 57.224365 73.775658 57.749012 73.549637 58.135708 C 73.324036 58.521591 72.775032 58.833626 71.67704 59.457287 C 71.360954 59.636627 71.05217 59.828156 70.750282 60.03022 C 69.596283 60.804417 69.019691 61.191113 68.527908 61.202473 C 68.03653 61.213436 67.537842 60.910736 66.539658 60.305332 L 65.790215 59.850468 C 64.381805 58.995926 63.678211 58.568665 62.984756 58.752872 C 62.291718 58.936691 61.896912 59.655701 61.106888 61.094131 L 59.376701 64.243683 C 58.655254 65.557953 58.294128 66.214478 58.466984 66.8564 C 58.639843 67.497498 59.282974 67.887848 60.569241 68.668533 L 62.184597 69.648056 C 63.054142 70.175949 63.488724 70.439697 63.702965 70.823547 C 63.916798 71.208214 63.910309 71.795761 63.896515 72.971664 L 63.89529 73.164406 C 63.89529 73.500778 63.905849 73.834724 63.92532 74.166237 C 64.001198 75.413147 64.038933 76.0364 63.823883 76.445007 C 63.60923 76.852798 63.148285 77.13237 62.227604 77.691513 L 60.568439 78.698212 C 59.282566 79.478905 58.639431 79.868843 58.466167 80.510361 C 58.29372 81.151459 58.654041 81.808403 59.375889 83.122261 L 61.106075 86.271805 C 61.896099 87.710243 62.291306 88.429245 62.984356 88.613464 C 63.677395 88.797279 64.381401 88.370415 65.789406 87.515877 L 67.365387 86.559898 C 68.305542 85.988991 68.775826 85.703735 69.243668 85.702515 C 69.711113 85.700485 70.273506 86.036865 71.399094 86.708809 C 71.490791 86.764 71.583305 86.817963 71.67704 86.871117 C 72.775032 87.495178 73.324036 87.806404 73.549637 88.1931 C 73.775658 88.579796 73.775658 89.104446 73.775658 90.153748 L 73.775658 92.678413 C 73.775658 94.244263 73.775658 95.026978 74.265823 95.513489 C 74.755569 96 75.54438 96 77.121986 96 L 80.305611 96 C 81.883232 96 82.67244 96 83.161797 95.513489 C 83.651955 95.026978 83.651955 94.244263 83.651955 92.678413 L 83.651955 90.149696 C 83.651955 89.100792 83.651955 88.576141 83.87796 88.18985 C 84.103569 87.803558 84.652168 87.491936 85.749344 86.867867 C 85.840645 86.815529 85.931534 86.763184 86.021622 86.708809 C 87.146797 86.036865 87.70919 85.70089 88.177444 85.70211 C 88.644882 85.703735 89.115166 85.988991 90.055725 86.559494 L 91.632523 87.516281 C 93.04052 88.370827 93.744522 88.797684 94.437576 88.613464 C 95.130211 88.429657 95.525017 87.710648 96.315041 86.272614 L 98.046028 83.122673 C 98.767479 81.809212 99.128212 81.151871 98.955353 80.510757 C 98.782494 79.869247 98.139366 79.478905 96.853088 78.698616 L 95.192703 77.691109 C 94.272026 77.13237 93.811073 76.852386 93.59642 76.444595 C 93.381775 76.035995 93.41951 75.412743 93.49498 74.165825 C 93.514862 73.834724 93.525002 73.500778 93.525002 73.164406 L 93.523788 72.973694 C 93.510406 71.798195 93.503502 71.209831 93.718147 70.825989 C 93.931992 70.442131 94.36657 70.178383 95.236115 69.650894 L 96.854713 68.668533 C 98.140976 67.887848 98.784119 67.497498 98.957382 66.8564 C 99.129822 66.214882 98.769508 65.557953 98.047653 64.24408 L 96.317078 61.094543 C 95.527054 59.656109 95.132248 58.937092 94.439194 58.752872 C 93.746147 58.569065 93.042152 58.995926 91.634148 59.850468 L 90.882675 60.306545 C 89.884094 60.91235 89.384995 61.21505 88.893616 61.20369 C 88.402237 61.192738 87.825241 60.805641 86.671654 60.031033 C 86.370979 59.829773 86.063416 59.639469 85.749344 59.460938 C 84.652168 58.836868 84.103569 58.524837 83.87796 58.13855 C 83.651955 57.752262 83.651955 57.227604 83.651955 56.178707 L 83.651955 54.687527 C 83.651955 53.121677 83.651955 52.338959 83.161797 51.852451 C 82.67244 51.36594 81.883232 51.36594 80.305611 51.36594 L 77.121986 51.36594 C 75.54438 51.36594 74.755569 51.36594 74.265823 51.852451 Z M 78.629311 83.265717 C 85.086212 83.265717 88.314659 77.592979 88.314659 73.580376 C 88.314659 69.567368 85.086212 63.895035 78.629311 63.895035 C 72.172417 63.895035 68.94397 69.567368 68.94397 73.580376 C 68.94397 77.592979 72.172417 83.265717 78.629311 83.265717 Z"/>
6 +</svg>
customer_portal/src/assets/icons/brain-icon.svg new
+16
@@ -0,0 +1,16 @@
1 +<svg width="24" height="24" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
2 +<path d="M26.7115 21C25.4668 21 24.3695 20.1584 24.1635 18.9744C24.0555 18.3504 24 17.7071 24 17.0628C24 9.86007 30.28 4 37.9995 4C45.719 4 52 9.86007 52 17.0628C52 18.3947 50.8431 19.4741 49.4157 19.4741C47.9883 19.4741 46.8315 18.3947 46.8315 17.0628C46.8315 12.5191 42.8692 8.82256 37.9995 8.82256C33.1298 8.82256 29.1685 12.5191 29.1685 17.0628C29.1685 17.449 29.2009 17.8324 29.2655 18.2021C29.4936 19.5165 28.5376 20.7551 27.1284 20.9684C26.9881 20.9896 26.8488 21 26.7115 21Z" fill="currentColor"/>
3 +<path d="M11.7443 63C11.3076 63 10.8662 62.8786 10.4692 62.6251C5.79222 59.6385 3 54.4778 3 48.8198C3 41.4767 7.64204 35.2807 14.2171 33.2687C13.8692 32.0435 13.6896 30.7644 13.6896 29.4697C13.6896 22.0426 19.5699 16 26.7981 16C31.9959 16 36.7042 19.1507 38.7941 24.0273C39.3328 25.2841 38.778 26.7512 37.5549 27.3048C36.3317 27.8574 34.9035 27.2883 34.3647 26.031C33.048 22.9585 30.0781 20.9728 26.7981 20.9728C22.2383 20.9728 18.5292 24.7844 18.5292 29.4697C18.5292 31.0213 18.9338 32.5325 19.6994 33.8398C20.1304 34.5746 20.1616 35.4861 19.7826 36.2505C19.4036 37.0148 18.6682 37.5242 17.8364 37.599C12.1376 38.1089 7.83959 42.9331 7.83959 48.8198C7.83959 52.7466 9.77732 56.3285 13.0232 58.4012C14.1585 59.1262 14.5073 60.6593 13.8012 61.8258C13.3427 62.5833 12.5525 63 11.7443 63Z" fill="currentColor"/>
4 +<path d="M23.1336 84C14.2378 84 7 76.5713 7 67.4404C7 61.6881 9.84205 56.4335 14.6016 53.3847C15.7568 52.6433 17.2816 53.0059 18.0054 54.1935C18.7272 55.3816 18.375 56.9461 17.2169 57.6874C13.9136 59.8032 11.941 63.4492 11.941 67.4404C11.941 73.7751 16.9621 78.9287 23.1336 78.9287C25.6524 78.9287 28.0302 78.0907 30.0105 76.5059C31.0856 75.6442 32.6393 75.8423 33.4779 76.9457C34.3165 78.0501 34.1245 79.6433 33.0485 80.5045C30.1929 82.7911 26.7641 84 23.1336 84Z" fill="currentColor"/>
5 +<path d="M35.9366 95C28.008 95 21.1679 89.61 20.0269 82.4625C19.8245 81.1941 20.785 80.0159 22.173 79.831C23.568 79.6437 24.8509 80.5236 25.0533 81.7925C25.8332 86.6757 30.5123 90.358 35.9366 90.358C41.9929 90.358 46.92 85.8556 46.92 80.321C46.92 79.039 48.0571 78 49.46 78C50.863 78 52 79.039 52 80.321C52 88.4151 44.7938 95 35.9366 95Z" fill="currentColor"/>
6 +<path d="M72.2885 21C72.1512 21 72.0119 20.9896 71.8716 20.9684C70.4624 20.7551 69.5064 19.5165 69.7345 18.2021C69.7992 17.8324 69.8315 17.449 69.8315 17.0628C69.8315 12.5191 65.8702 8.82256 61.0005 8.82256C56.1308 8.82256 52.1685 12.5191 52.1685 17.0628C52.1685 18.3947 51.0117 19.4741 49.5843 19.4741C48.1569 19.4741 47 18.3947 47 17.0628C47 9.86007 53.281 4 61.0005 4C68.72 4 75 9.86007 75 17.0628C75 17.7071 74.9445 18.3504 74.8365 18.9744C74.6305 20.1579 73.5332 21 72.2885 21Z" fill="currentColor"/>
7 +<path d="M87.3624 62C86.564 62 85.7835 61.5922 85.3306 60.8507C84.6331 59.7091 84.9777 58.2086 86.0991 57.499C89.3054 55.4705 91.2195 51.9647 91.2195 48.1215C91.2195 42.3601 86.9739 37.6385 81.3447 37.1395C80.523 37.0663 79.7966 36.5677 79.4222 35.8196C79.0478 35.0715 79.0786 34.1794 79.5043 33.4603C80.2606 32.1808 80.6603 30.7017 80.6603 29.1831C80.6603 24.5975 76.9964 20.867 72.4923 20.867C71.1085 20.867 69.7435 21.2225 68.5446 21.8955C67.3887 22.5443 65.9358 22.1165 65.2972 20.9392C64.6604 19.7624 65.0806 18.2819 66.2365 17.6331C68.1403 16.5646 70.3037 16 72.4923 16C79.6323 16 85.4408 21.914 85.4408 29.1831C85.4408 30.4502 85.2634 31.7022 84.9198 32.9013C91.4146 34.8704 96 40.9347 96 48.1215C96 53.6591 93.2419 58.71 88.6219 61.6331C88.2298 61.8812 87.7937 62 87.3624 62Z" fill="currentColor"/>
8 +<path d="M75.8664 84C72.2359 84 68.8071 82.7911 65.9515 80.5044C64.8755 79.6432 64.6835 78.0499 65.5221 76.9455C66.3607 75.8421 67.9144 75.644 68.9895 76.5057C70.9698 78.0905 73.3476 78.9285 75.8664 78.9285C82.0379 78.9285 87.059 73.7748 87.059 67.44C87.059 63.4487 85.0864 59.8025 81.7831 57.6868C80.625 56.9454 80.2728 55.3809 80.9946 54.1927C81.7184 53.0056 83.2432 52.644 84.3984 53.384C89.1579 56.4328 92 61.6875 92 67.44C92 76.5711 84.7622 84 75.8664 84Z" fill="currentColor"/>
9 +<path d="M63.0644 95C54.2062 95 47 88.4151 47 80.321C47 79.039 48.137 78 49.54 78C50.9429 78 52.08 79.039 52.08 80.321C52.08 85.8556 57.0071 90.358 63.0644 90.358C68.4887 90.358 73.1668 86.6757 73.9467 81.7925C74.1491 80.5241 75.431 79.6442 76.827 79.831C78.215 80.0159 79.1755 81.1941 78.9731 82.4625C77.8321 89.61 70.9929 95 63.0644 95Z" fill="currentColor"/>
10 +<path d="M30.333 64C29.178 64 28.1743 63.123 28.0201 61.9185C27.8523 60.6088 28.7528 59.4075 30.032 59.2356C33.7755 58.7319 35.6778 56.0785 37.8804 53.0059C40.4159 49.469 43.5819 45.0667 50.048 46.174C51.3208 46.3913 52.1803 47.6239 51.9677 48.927C51.7551 50.2306 50.5444 51.1136 49.2798 50.8931C45.7935 50.2983 44.2671 52.1799 41.6458 55.8374C39.2918 59.1221 36.3612 63.2099 30.6396 63.979C30.5365 63.993 30.4343 64 30.333 64Z" fill="currentColor"/>
11 +<path d="M41.5604 54C40.54 54 39.5892 53.4065 39.2481 52.4672C38.5498 50.5484 37.2655 49.1661 35.321 48.2413C34.1234 47.6724 33.6547 46.3177 34.272 45.2153C34.8903 44.1133 36.3652 43.6825 37.558 44.2502C40.6429 45.7167 42.7675 48.0009 43.8726 51.0397C44.3014 52.2163 43.6125 53.4889 42.3349 53.8833C42.0787 53.9623 41.8167 54 41.5604 54Z" fill="currentColor"/>
12 +<path d="M49.5 82C48.1191 82 47 81.0194 47 79.8096V77.1904C47 75.9806 48.1191 75 49.5 75C50.8809 75 52 75.9806 52 77.1904V79.8096C52 81.0194 50.8809 82 49.5 82Z" fill="currentColor"/>
13 +<path d="M49.5 68C48.1191 68 47 66.9081 47 65.5608V17.4392C47 16.0919 48.1191 15 49.5 15C50.8809 15 52 16.0919 52 17.4392V65.5608C52 66.9081 50.8809 68 49.5 68Z" fill="currentColor"/>
14 +<path d="M64.5696 46C64.4803 46 64.39 45.9952 64.2997 45.9851C59.7119 45.473 57.515 42.2325 55.7503 39.6285C53.7776 36.7183 52.7808 35.4583 49.9386 36.0679C48.6244 36.3477 47.3321 35.5029 47.0537 34.1743C46.7743 32.8466 47.6134 31.542 48.9276 31.2602C55.0842 29.9388 57.7877 33.9347 59.7642 36.8512C61.4385 39.3213 62.5798 40.8482 64.8338 41.0997C66.1689 41.249 67.1324 42.4634 66.9851 43.8122C66.8473 45.0698 65.7935 46 64.5696 46Z" fill="currentColor"/>
15 +<path d="M64.5688 65C63.6993 65 62.8574 64.5119 62.4202 63.6541C61.1611 61.1881 59.6825 59.7058 58.0252 59.248C56.1114 58.7187 53.6568 59.43 50.7329 61.3608C49.6011 62.1089 48.0949 61.7581 47.3765 60.571C46.659 59.3854 46.9973 57.8168 48.1329 57.0667C52.2485 54.3502 55.9955 53.4324 59.2701 54.3373C62.2882 55.1712 64.7931 57.5018 66.7136 61.265C67.3455 62.5027 66.897 64.0416 65.711 64.7009C65.3471 64.9038 64.9546 65 64.5688 65Z" fill="currentColor"/>
16 +</svg>
customer_portal/src/assets/images/Ripple-2s-100px.svg new
+1
@@ -0,0 +1 @@
1 +<svg width="100px" height="100px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="lds-ripple" style="background: none;"><circle cx="50" cy="50" r="28.6685" fill="none" ng-attr-stroke="{{config.c1}}" ng-attr-stroke-width="{{config.width}}" stroke="#4a596a" stroke-width="2"><animate attributeName="r" calcMode="spline" values="0;30" keyTimes="0;1" dur="2" keySplines="0 0.2 0.8 1" begin="-1s" repeatCount="indefinite"></animate><animate attributeName="opacity" calcMode="spline" values="1;0" keyTimes="0;1" dur="2" keySplines="0.2 0 0.8 1" begin="-1s" repeatCount="indefinite"></animate></circle><circle cx="50" cy="50" r="15.9828" fill="none" ng-attr-stroke="{{config.c2}}" ng-attr-stroke-width="{{config.width}}" stroke="#89ade8" stroke-width="2"><animate attributeName="r" calcMode="spline" values="0;30" keyTimes="0;1" dur="2" keySplines="0 0.2 0.8 1" begin="0s" repeatCount="indefinite"></animate><animate attributeName="opacity" calcMode="spline" values="1;0" keyTimes="0;1" dur="2" keySplines="0.2 0 0.8 1" begin="0s" repeatCount="indefinite"></animate></circle></svg>
customer_portal/src/assets/images/copilot_logo.PNG
Binary files /dev/null and b/customer_portal/src/assets/images/copilot_logo.PNG differ
customer_portal/src/assets/images/img-not-found.svg new
+1
@@ -0,0 +1 @@
1 +<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 600 600" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.41421;"><rect x="0" y="0" width="600" height="600" style="fill:#e2e3e5;"/><circle cx="300" cy="300" r="155" style="fill:none;stroke-width:47px;stroke:#bcbdbf;"/><rect x="160" y="223" width="275" height="166" style="fill:#e3e5e4;"/><path d="M440.5,394.5l-286,0l0,-177l286,0l0,177ZM165.5,228.5l0,155l264,0l0,-155l-264,0Z" style="fill:#fff;"/><path d="M404.434,170.877l-232.94,232.941l35.355,35.355l232.941,-232.94l-35.356,-35.356Z" style="fill:#bcbdbf;"/></svg>
customer_portal/src/assets/images/pattern-onboard.png
Binary files /dev/null and b/customer_portal/src/assets/images/pattern-onboard.png differ
customer_portal/src/assets/images/placeholder.png
Binary files /dev/null and b/customer_portal/src/assets/images/placeholder.png differ
customer_portal/src/assets/images/socfortress_logo_default_dark.svg new
+3
@@ -0,0 +1,3 @@
1 +<?xml version="1.0" standalone="no"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg" version="1.1" width="400" height="497.1875" viewBox="0, 0, 400,497.1875"><g id="svgg"><path id="path0" d="M163.187 283.504 C 163.117 320.247,163.007 318.404,165.423 320.832 C 165.964 321.375,166.881 322.392,167.461 323.091 C 168.041 323.791,168.642 324.285,168.797 324.190 C 168.952 324.094,169.045 324.132,169.004 324.274 C 168.877 324.716,169.816 325.938,170.284 325.938 C 170.529 325.938,170.635 326.032,170.520 326.147 C 170.405 326.261,170.544 326.613,170.829 326.928 C 171.324 327.474,172.623 327.500,199.952 327.500 L 228.557 327.500 229.669 326.299 C 237.848 317.465,236.850 323.401,236.796 283.906 C 236.771 265.688,236.702 251.801,236.643 253.047 L 236.535 255.313 200.000 255.313 L 163.465 255.313 163.357 253.047 C 163.298 251.801,163.221 265.506,163.187 283.504 M0.313 265.477 C 0.313 275.210,0.341 275.707,0.973 276.966 C 2.064 279.137,3.126 280.334,4.489 280.930 C 5.200 281.240,7.750 282.439,10.156 283.594 C 12.563 284.749,16.289 286.538,18.438 287.569 C 20.586 288.600,23.328 289.905,24.531 290.469 C 25.734 291.033,29.180 292.679,32.188 294.128 C 35.195 295.576,38.254 297.025,38.984 297.347 L 40.313 297.933 40.382 317.326 C 40.420 327.992,40.494 334.645,40.546 332.109 L 40.642 327.500 86.406 327.500 L 132.171 327.500 132.278 332.109 C 132.337 334.645,132.364 323.500,132.338 307.344 C 132.296 281.827,132.223 277.579,131.783 275.000 C 130.884 269.738,130.032 267.327,127.695 263.438 C 126.365 261.225,123.870 258.438,123.220 258.438 C 122.996 258.438,122.813 258.313,122.813 258.161 C 122.812 258.008,122.355 257.653,121.797 257.371 C 120.339 256.634,119.564 256.139,119.308 255.781 C 119.161 255.575,98.839 255.441,59.699 255.387 L 0.313 255.305 0.313 265.477 M280.703 255.737 C 280.316 255.929,280.000 256.193,280.000 256.324 C 280.000 256.455,279.812 256.563,279.582 256.563 C 278.920 256.563,275.625 259.049,275.625 259.548 C 275.625 259.797,275.438 260.000,275.209 260.000 C 274.444 260.000,272.173 263.141,270.916 265.938 C 270.568 266.711,270.166 267.562,270.021 267.829 C 269.668 268.483,268.526 273.110,268.097 275.625 C 267.864 276.992,267.728 287.319,267.682 307.188 C 267.644 323.430,267.661 334.645,267.721 332.109 L 267.829 327.500 313.594 327.500 L 359.358 327.500 359.454 332.109 C 359.506 334.645,359.582 328.000,359.622 317.344 L 359.695 297.969 360.863 297.423 C 363.848 296.030,370.955 292.650,375.739 290.348 C 378.638 288.954,381.044 287.813,381.087 287.813 C 381.131 287.813,384.103 286.390,387.692 284.652 C 391.282 282.914,394.922 281.178,395.781 280.794 C 399.749 279.022,399.869 278.559,399.757 265.445 L 399.671 255.313 340.539 255.351 C 297.771 255.379,281.212 255.486,280.703 255.737 M172.721 329.024 C 173.441 329.716,173.926 329.912,173.595 329.377 C 173.489 329.206,173.094 328.907,172.717 328.713 L 172.031 328.360 172.721 329.024 " stroke="none" fill="#fbab44" fill-rule="evenodd"/><path id="path1" d="M0.207 176.484 C 0.094 180.223,0.034 204.094,0.074 229.531 C 0.115 254.969,0.186 271.175,0.234 265.545 L 0.321 255.309 59.223 255.389 C 91.619 255.433,118.125 255.435,118.125 255.395 C 118.125 255.251,115.844 254.370,115.142 254.243 C 114.750 254.172,114.142 253.960,113.790 253.771 C 113.438 253.583,113.039 253.448,112.903 253.470 C 112.767 253.493,112.586 253.484,112.500 253.451 C 111.792 253.175,104.237 251.869,103.433 251.884 C 103.006 251.892,101.953 251.817,101.094 251.717 C 99.332 251.512,96.400 251.225,94.531 251.076 C 93.844 251.021,92.508 250.892,91.563 250.789 C 90.617 250.686,89.281 250.542,88.594 250.469 C 68.487 248.333,56.970 240.804,48.921 224.531 C 42.866 212.290,38.598 191.192,38.317 172.109 L 38.281 169.688 19.347 169.688 L 0.412 169.688 0.207 176.484 M39.844 170.000 C 39.950 170.172,40.240 170.313,40.487 170.313 C 40.735 170.313,40.938 170.418,40.939 170.547 C 40.942 170.889,42.824 171.905,43.117 171.724 C 43.254 171.639,43.540 171.779,43.753 172.035 C 43.965 172.291,44.262 172.500,44.413 172.500 C 44.564 172.500,44.969 172.781,45.313 173.125 C 45.656 173.469,46.212 173.750,46.547 173.750 C 46.882 173.750,47.269 173.940,47.406 174.173 C 47.544 174.405,48.310 174.957,49.110 175.399 C 49.909 175.841,51.136 176.565,51.838 177.008 C 52.539 177.450,53.189 177.813,53.282 177.813 C 53.375 177.813,53.975 178.147,54.616 178.556 C 55.257 178.964,58.172 180.669,61.094 182.345 C 64.016 184.020,68.023 186.338,70.000 187.495 C 74.085 189.886,97.436 203.374,124.375 218.902 C 147.524 232.246,148.698 232.924,151.719 234.704 C 153.094 235.514,154.790 236.464,155.489 236.815 C 160.132 239.149,162.895 244.302,163.298 251.380 L 163.522 255.313 199.996 255.313 L 236.471 255.313 236.688 251.641 C 237.236 242.355,238.811 240.161,249.253 234.146 C 264.241 225.511,277.317 217.958,288.906 211.239 C 290.994 210.029,296.178 207.048,303.281 202.973 C 307.406 200.607,312.398 197.720,314.375 196.559 C 316.352 195.398,318.461 194.168,319.063 193.826 C 319.664 193.484,322.898 191.618,326.250 189.679 C 329.602 187.741,332.766 185.916,333.281 185.625 C 334.295 185.052,341.339 180.967,343.125 179.916 C 343.727 179.563,345.977 178.235,348.125 176.967 C 350.273 175.698,352.313 174.495,352.656 174.292 C 353.000 174.090,353.809 173.602,354.453 173.208 C 355.098 172.814,355.625 172.590,355.625 172.710 C 355.625 172.831,356.030 172.542,356.525 172.068 C 357.311 171.314,357.854 171.055,358.847 170.961 C 358.986 170.948,359.013 170.798,358.908 170.629 C 358.799 170.451,358.924 170.399,359.203 170.506 C 359.469 170.608,359.688 170.547,359.688 170.369 C 359.688 170.191,359.934 169.982,360.234 169.903 C 360.535 169.824,288.527 169.744,200.216 169.724 C 93.243 169.700,39.715 169.792,39.844 170.000 M361.724 171.954 C 361.726 173.200,361.637 175.063,361.525 176.094 C 361.413 177.125,361.234 179.336,361.127 181.006 C 358.825 217.064,347.461 239.582,327.963 246.718 C 325.065 247.779,324.284 248.053,323.910 248.143 C 323.830 248.162,323.023 248.383,322.116 248.633 C 321.210 248.884,320.117 249.142,319.688 249.208 C 319.258 249.273,317.641 249.538,316.094 249.797 C 313.333 250.258,311.392 250.513,308.281 250.822 C 306.745 250.975,306.251 251.017,302.188 251.334 C 300.248 251.485,299.357 251.578,295.577 252.021 C 293.281 252.291,288.384 253.131,287.813 253.354 C 287.555 253.454,287.168 253.508,286.953 253.473 C 286.738 253.438,286.563 253.572,286.563 253.770 C 286.563 253.968,286.365 254.054,286.124 253.961 C 285.883 253.869,285.598 253.881,285.490 253.989 C 285.383 254.097,285.123 254.216,284.913 254.254 C 279.486 255.244,280.800 255.268,340.855 255.290 L 399.679 255.313 399.766 265.391 C 399.814 270.934,399.886 254.727,399.926 229.375 C 399.966 204.023,399.906 180.223,399.793 176.484 L 399.588 169.688 380.653 169.688 L 361.719 169.689 361.724 171.954 " stroke="none" fill="#fb9b3c" fill-rule="evenodd"/><path id="path2" d="M43.051 418.013 C 42.581 418.263,41.808 418.977,41.333 419.600 L 40.469 420.732 40.387 456.698 L 40.305 492.664 41.012 494.048 C 41.401 494.809,42.116 495.722,42.601 496.075 L 43.483 496.719 199.724 496.798 C 338.186 496.868,356.068 496.823,356.873 496.408 C 357.372 496.149,358.174 495.422,358.656 494.790 L 359.531 493.642 359.531 457.212 L 359.531 420.781 358.631 419.605 C 356.922 417.371,358.257 417.478,333.366 417.572 C 308.066 417.667,309.971 417.467,308.594 420.176 C 307.992 421.360,307.965 422.117,307.881 440.391 L 307.794 459.375 289.060 459.375 L 270.326 459.375 270.241 439.914 L 270.156 420.453 269.428 419.523 C 267.785 417.426,269.037 417.528,244.756 417.513 C 220.973 417.499,221.546 417.460,220.063 419.198 C 218.773 420.710,218.750 421.077,218.750 440.489 L 218.750 459.375 200.000 459.375 L 181.250 459.375 181.250 440.507 C 181.250 418.749,181.296 419.072,178.027 417.833 C 176.227 417.152,133.830 417.407,132.284 418.108 C 129.592 419.331,129.688 418.513,129.688 440.300 L 129.688 459.375 110.938 459.375 L 92.188 459.375 92.188 440.973 C 92.188 420.244,92.175 420.112,90.046 418.489 L 88.955 417.656 66.431 417.608 C 47.275 417.567,43.778 417.628,43.051 418.013 " stroke="none" fill="#fccf5b" fill-rule="evenodd"/><path id="path3" d="M2.859 0.840 C 2.283 1.125,1.439 1.922,0.984 2.610 L 0.156 3.861 0.146 91.383 C 0.140 139.521,0.179 176.834,0.232 174.302 L 0.329 169.697 19.461 169.614 C 29.984 169.569,38.760 169.602,38.962 169.688 C 39.165 169.773,111.798 169.773,200.368 169.688 C 288.939 169.602,370.016 169.569,380.539 169.614 L 399.671 169.697 399.768 174.302 C 399.821 176.834,399.860 139.521,399.854 91.383 L 399.844 3.861 399.012 2.604 C 397.377 0.133,399.990 0.294,363.069 0.386 L 330.156 0.469 329.104 1.219 C 326.750 2.898,326.890 0.648,326.882 37.109 L 326.875 69.688 308.906 69.688 L 290.938 69.688 290.938 44.441 L 290.937 19.195 290.234 17.802 C 289.772 16.886,289.103 16.169,288.281 15.707 L 287.031 15.006 254.163 15.081 C 217.137 15.166,220.575 14.931,218.828 17.497 L 218.125 18.530 218.125 44.109 L 218.125 69.688 200.000 69.688 L 181.875 69.688 181.875 44.109 L 181.875 18.530 181.172 17.497 C 179.425 14.931,182.863 15.166,145.837 15.081 L 112.969 15.006 111.719 15.707 C 110.897 16.169,110.228 16.886,109.766 17.802 L 109.062 19.195 109.063 44.441 L 109.063 69.688 91.094 69.688 L 73.125 69.688 73.118 37.109 C 73.110 0.648,73.250 2.898,70.896 1.219 L 69.844 0.469 36.875 0.395 C 6.708 0.328,3.817 0.366,2.859 0.840 M361.648 171.250 C 361.648 172.023,361.709 172.340,361.783 171.953 C 361.858 171.566,361.858 170.934,361.783 170.547 C 361.709 170.160,361.648 170.477,361.648 171.250 M357.144 171.568 L 356.406 172.198 357.266 171.830 C 357.738 171.628,358.125 171.344,358.125 171.200 C 358.125 170.810,357.958 170.873,357.144 171.568 M43.125 171.759 C 43.125 171.921,43.547 172.302,44.063 172.607 C 44.580 172.912,45.000 173.012,45.000 172.830 C 45.000 172.649,44.835 172.500,44.634 172.500 C 44.433 172.500,44.011 172.267,43.696 171.983 C 43.382 171.698,43.125 171.598,43.125 171.759 " stroke="none" fill="#fc862e" fill-rule="evenodd"/><path id="path4" d="M40.519 334.297 C 40.406 338.035,40.317 349.602,40.320 360.000 C 40.327 381.137,40.273 380.601,42.542 382.218 L 43.594 382.969 86.025 383.050 C 123.482 383.122,128.563 383.076,129.374 382.657 C 130.775 381.932,131.789 380.671,132.162 379.189 C 132.542 377.680,132.636 345.641,132.293 334.297 L 132.088 327.500 86.406 327.500 L 40.725 327.500 40.519 334.297 M172.522 328.457 C 173.111 328.983,173.594 329.525,173.594 329.661 C 173.594 329.965,174.624 331.148,183.281 340.783 C 185.000 342.696,187.250 345.224,188.281 346.401 C 189.313 347.577,190.302 348.692,190.481 348.879 C 190.660 349.066,191.920 350.484,193.281 352.031 C 196.329 355.495,196.773 355.907,198.037 356.446 C 200.772 357.613,202.678 356.646,206.742 352.031 C 208.407 350.141,210.278 348.028,210.900 347.335 C 211.522 346.643,212.664 345.373,213.438 344.512 C 214.211 343.651,215.941 341.731,217.282 340.245 C 218.623 338.758,220.029 337.148,220.407 336.665 C 221.032 335.867,223.081 333.572,224.453 332.135 C 224.754 331.820,224.941 331.563,224.868 331.563 C 224.751 331.563,225.925 330.264,227.905 328.203 L 228.581 327.500 200.015 327.500 L 171.450 327.500 172.522 328.457 M267.707 334.297 C 267.364 345.641,267.458 377.680,267.838 379.189 C 268.211 380.671,269.225 381.932,270.626 382.657 C 271.437 383.076,276.518 383.122,313.975 383.050 L 356.406 382.969 357.458 382.218 C 359.727 380.601,359.673 381.137,359.680 360.000 C 359.683 349.602,359.594 338.035,359.481 334.297 L 359.275 327.500 313.594 327.500 L 267.912 327.500 267.707 334.297 " stroke="none" fill="#fcbc4c" fill-rule="evenodd"/></g></svg>
customer_portal/src/assets/images/socfortress_logo_default_light.svg new
+3
@@ -0,0 +1,3 @@
1 +<?xml version="1.0" standalone="no"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg" version="1.1" width="400" height="497.1875" viewBox="0, 0, 400,497.1875"><g id="svgg"><path id="path0" d="M163.187 283.504 C 163.117 320.247,163.007 318.404,165.423 320.832 C 165.964 321.375,166.881 322.392,167.461 323.091 C 168.041 323.791,168.642 324.285,168.797 324.190 C 168.952 324.094,169.045 324.132,169.004 324.274 C 168.877 324.716,169.816 325.938,170.284 325.938 C 170.529 325.938,170.635 326.032,170.520 326.147 C 170.405 326.261,170.544 326.613,170.829 326.928 C 171.324 327.474,172.623 327.500,199.952 327.500 L 228.557 327.500 229.669 326.299 C 237.848 317.465,236.850 323.401,236.796 283.906 C 236.771 265.688,236.702 251.801,236.643 253.047 L 236.535 255.313 200.000 255.313 L 163.465 255.313 163.357 253.047 C 163.298 251.801,163.221 265.506,163.187 283.504 M0.313 265.477 C 0.313 275.210,0.341 275.707,0.973 276.966 C 2.064 279.137,3.126 280.334,4.489 280.930 C 5.200 281.240,7.750 282.439,10.156 283.594 C 12.563 284.749,16.289 286.538,18.438 287.569 C 20.586 288.600,23.328 289.905,24.531 290.469 C 25.734 291.033,29.180 292.679,32.188 294.128 C 35.195 295.576,38.254 297.025,38.984 297.347 L 40.313 297.933 40.382 317.326 C 40.420 327.992,40.494 334.645,40.546 332.109 L 40.642 327.500 86.406 327.500 L 132.171 327.500 132.278 332.109 C 132.337 334.645,132.364 323.500,132.338 307.344 C 132.296 281.827,132.223 277.579,131.783 275.000 C 130.884 269.738,130.032 267.327,127.695 263.438 C 126.365 261.225,123.870 258.438,123.220 258.438 C 122.996 258.438,122.813 258.313,122.813 258.161 C 122.812 258.008,122.355 257.653,121.797 257.371 C 120.339 256.634,119.564 256.139,119.308 255.781 C 119.161 255.575,98.839 255.441,59.699 255.387 L 0.313 255.305 0.313 265.477 M280.703 255.737 C 280.316 255.929,280.000 256.193,280.000 256.324 C 280.000 256.455,279.812 256.563,279.582 256.563 C 278.920 256.563,275.625 259.049,275.625 259.548 C 275.625 259.797,275.438 260.000,275.209 260.000 C 274.444 260.000,272.173 263.141,270.916 265.938 C 270.568 266.711,270.166 267.562,270.021 267.829 C 269.668 268.483,268.526 273.110,268.097 275.625 C 267.864 276.992,267.728 287.319,267.682 307.188 C 267.644 323.430,267.661 334.645,267.721 332.109 L 267.829 327.500 313.594 327.500 L 359.358 327.500 359.454 332.109 C 359.506 334.645,359.582 328.000,359.622 317.344 L 359.695 297.969 360.863 297.423 C 363.848 296.030,370.955 292.650,375.739 290.348 C 378.638 288.954,381.044 287.813,381.087 287.813 C 381.131 287.813,384.103 286.390,387.692 284.652 C 391.282 282.914,394.922 281.178,395.781 280.794 C 399.749 279.022,399.869 278.559,399.757 265.445 L 399.671 255.313 340.539 255.351 C 297.771 255.379,281.212 255.486,280.703 255.737 M172.721 329.024 C 173.441 329.716,173.926 329.912,173.595 329.377 C 173.489 329.206,173.094 328.907,172.717 328.713 L 172.031 328.360 172.721 329.024 " stroke="none" fill="#fbab44" fill-rule="evenodd"/><path id="path1" d="M0.207 176.484 C 0.094 180.223,0.034 204.094,0.074 229.531 C 0.115 254.969,0.186 271.175,0.234 265.545 L 0.321 255.309 59.223 255.389 C 91.619 255.433,118.125 255.435,118.125 255.395 C 118.125 255.251,115.844 254.370,115.142 254.243 C 114.750 254.172,114.142 253.960,113.790 253.771 C 113.438 253.583,113.039 253.448,112.903 253.470 C 112.767 253.493,112.586 253.484,112.500 253.451 C 111.792 253.175,104.237 251.869,103.433 251.884 C 103.006 251.892,101.953 251.817,101.094 251.717 C 99.332 251.512,96.400 251.225,94.531 251.076 C 93.844 251.021,92.508 250.892,91.563 250.789 C 90.617 250.686,89.281 250.542,88.594 250.469 C 68.487 248.333,56.970 240.804,48.921 224.531 C 42.866 212.290,38.598 191.192,38.317 172.109 L 38.281 169.688 19.347 169.688 L 0.412 169.688 0.207 176.484 M39.844 170.000 C 39.950 170.172,40.240 170.313,40.487 170.313 C 40.735 170.313,40.938 170.418,40.939 170.547 C 40.942 170.889,42.824 171.905,43.117 171.724 C 43.254 171.639,43.540 171.779,43.753 172.035 C 43.965 172.291,44.262 172.500,44.413 172.500 C 44.564 172.500,44.969 172.781,45.313 173.125 C 45.656 173.469,46.212 173.750,46.547 173.750 C 46.882 173.750,47.269 173.940,47.406 174.173 C 47.544 174.405,48.310 174.957,49.110 175.399 C 49.909 175.841,51.136 176.565,51.838 177.008 C 52.539 177.450,53.189 177.813,53.282 177.813 C 53.375 177.813,53.975 178.147,54.616 178.556 C 55.257 178.964,58.172 180.669,61.094 182.345 C 64.016 184.020,68.023 186.338,70.000 187.495 C 74.085 189.886,97.436 203.374,124.375 218.902 C 147.524 232.246,148.698 232.924,151.719 234.704 C 153.094 235.514,154.790 236.464,155.489 236.815 C 160.132 239.149,162.895 244.302,163.298 251.380 L 163.522 255.313 199.996 255.313 L 236.471 255.313 236.688 251.641 C 237.236 242.355,238.811 240.161,249.253 234.146 C 264.241 225.511,277.317 217.958,288.906 211.239 C 290.994 210.029,296.178 207.048,303.281 202.973 C 307.406 200.607,312.398 197.720,314.375 196.559 C 316.352 195.398,318.461 194.168,319.063 193.826 C 319.664 193.484,322.898 191.618,326.250 189.679 C 329.602 187.741,332.766 185.916,333.281 185.625 C 334.295 185.052,341.339 180.967,343.125 179.916 C 343.727 179.563,345.977 178.235,348.125 176.967 C 350.273 175.698,352.313 174.495,352.656 174.292 C 353.000 174.090,353.809 173.602,354.453 173.208 C 355.098 172.814,355.625 172.590,355.625 172.710 C 355.625 172.831,356.030 172.542,356.525 172.068 C 357.311 171.314,357.854 171.055,358.847 170.961 C 358.986 170.948,359.013 170.798,358.908 170.629 C 358.799 170.451,358.924 170.399,359.203 170.506 C 359.469 170.608,359.688 170.547,359.688 170.369 C 359.688 170.191,359.934 169.982,360.234 169.903 C 360.535 169.824,288.527 169.744,200.216 169.724 C 93.243 169.700,39.715 169.792,39.844 170.000 M361.724 171.954 C 361.726 173.200,361.637 175.063,361.525 176.094 C 361.413 177.125,361.234 179.336,361.127 181.006 C 358.825 217.064,347.461 239.582,327.963 246.718 C 325.065 247.779,324.284 248.053,323.910 248.143 C 323.830 248.162,323.023 248.383,322.116 248.633 C 321.210 248.884,320.117 249.142,319.688 249.208 C 319.258 249.273,317.641 249.538,316.094 249.797 C 313.333 250.258,311.392 250.513,308.281 250.822 C 306.745 250.975,306.251 251.017,302.188 251.334 C 300.248 251.485,299.357 251.578,295.577 252.021 C 293.281 252.291,288.384 253.131,287.813 253.354 C 287.555 253.454,287.168 253.508,286.953 253.473 C 286.738 253.438,286.563 253.572,286.563 253.770 C 286.563 253.968,286.365 254.054,286.124 253.961 C 285.883 253.869,285.598 253.881,285.490 253.989 C 285.383 254.097,285.123 254.216,284.913 254.254 C 279.486 255.244,280.800 255.268,340.855 255.290 L 399.679 255.313 399.766 265.391 C 399.814 270.934,399.886 254.727,399.926 229.375 C 399.966 204.023,399.906 180.223,399.793 176.484 L 399.588 169.688 380.653 169.688 L 361.719 169.689 361.724 171.954 " stroke="none" fill="#fb9b3c" fill-rule="evenodd"/><path id="path2" d="M43.051 418.013 C 42.581 418.263,41.808 418.977,41.333 419.600 L 40.469 420.732 40.387 456.698 L 40.305 492.664 41.012 494.048 C 41.401 494.809,42.116 495.722,42.601 496.075 L 43.483 496.719 199.724 496.798 C 338.186 496.868,356.068 496.823,356.873 496.408 C 357.372 496.149,358.174 495.422,358.656 494.790 L 359.531 493.642 359.531 457.212 L 359.531 420.781 358.631 419.605 C 356.922 417.371,358.257 417.478,333.366 417.572 C 308.066 417.667,309.971 417.467,308.594 420.176 C 307.992 421.360,307.965 422.117,307.881 440.391 L 307.794 459.375 289.060 459.375 L 270.326 459.375 270.241 439.914 L 270.156 420.453 269.428 419.523 C 267.785 417.426,269.037 417.528,244.756 417.513 C 220.973 417.499,221.546 417.460,220.063 419.198 C 218.773 420.710,218.750 421.077,218.750 440.489 L 218.750 459.375 200.000 459.375 L 181.250 459.375 181.250 440.507 C 181.250 418.749,181.296 419.072,178.027 417.833 C 176.227 417.152,133.830 417.407,132.284 418.108 C 129.592 419.331,129.688 418.513,129.688 440.300 L 129.688 459.375 110.938 459.375 L 92.188 459.375 92.188 440.973 C 92.188 420.244,92.175 420.112,90.046 418.489 L 88.955 417.656 66.431 417.608 C 47.275 417.567,43.778 417.628,43.051 418.013 " stroke="none" fill="#fccf5b" fill-rule="evenodd"/><path id="path3" d="M2.859 0.840 C 2.283 1.125,1.439 1.922,0.984 2.610 L 0.156 3.861 0.146 91.383 C 0.140 139.521,0.179 176.834,0.232 174.302 L 0.329 169.697 19.461 169.614 C 29.984 169.569,38.760 169.602,38.962 169.688 C 39.165 169.773,111.798 169.773,200.368 169.688 C 288.939 169.602,370.016 169.569,380.539 169.614 L 399.671 169.697 399.768 174.302 C 399.821 176.834,399.860 139.521,399.854 91.383 L 399.844 3.861 399.012 2.604 C 397.377 0.133,399.990 0.294,363.069 0.386 L 330.156 0.469 329.104 1.219 C 326.750 2.898,326.890 0.648,326.882 37.109 L 326.875 69.688 308.906 69.688 L 290.938 69.688 290.938 44.441 L 290.937 19.195 290.234 17.802 C 289.772 16.886,289.103 16.169,288.281 15.707 L 287.031 15.006 254.163 15.081 C 217.137 15.166,220.575 14.931,218.828 17.497 L 218.125 18.530 218.125 44.109 L 218.125 69.688 200.000 69.688 L 181.875 69.688 181.875 44.109 L 181.875 18.530 181.172 17.497 C 179.425 14.931,182.863 15.166,145.837 15.081 L 112.969 15.006 111.719 15.707 C 110.897 16.169,110.228 16.886,109.766 17.802 L 109.062 19.195 109.063 44.441 L 109.063 69.688 91.094 69.688 L 73.125 69.688 73.118 37.109 C 73.110 0.648,73.250 2.898,70.896 1.219 L 69.844 0.469 36.875 0.395 C 6.708 0.328,3.817 0.366,2.859 0.840 M361.648 171.250 C 361.648 172.023,361.709 172.340,361.783 171.953 C 361.858 171.566,361.858 170.934,361.783 170.547 C 361.709 170.160,361.648 170.477,361.648 171.250 M357.144 171.568 L 356.406 172.198 357.266 171.830 C 357.738 171.628,358.125 171.344,358.125 171.200 C 358.125 170.810,357.958 170.873,357.144 171.568 M43.125 171.759 C 43.125 171.921,43.547 172.302,44.063 172.607 C 44.580 172.912,45.000 173.012,45.000 172.830 C 45.000 172.649,44.835 172.500,44.634 172.500 C 44.433 172.500,44.011 172.267,43.696 171.983 C 43.382 171.698,43.125 171.598,43.125 171.759 " stroke="none" fill="#fc862e" fill-rule="evenodd"/><path id="path4" d="M40.519 334.297 C 40.406 338.035,40.317 349.602,40.320 360.000 C 40.327 381.137,40.273 380.601,42.542 382.218 L 43.594 382.969 86.025 383.050 C 123.482 383.122,128.563 383.076,129.374 382.657 C 130.775 381.932,131.789 380.671,132.162 379.189 C 132.542 377.680,132.636 345.641,132.293 334.297 L 132.088 327.500 86.406 327.500 L 40.725 327.500 40.519 334.297 M172.522 328.457 C 173.111 328.983,173.594 329.525,173.594 329.661 C 173.594 329.965,174.624 331.148,183.281 340.783 C 185.000 342.696,187.250 345.224,188.281 346.401 C 189.313 347.577,190.302 348.692,190.481 348.879 C 190.660 349.066,191.920 350.484,193.281 352.031 C 196.329 355.495,196.773 355.907,198.037 356.446 C 200.772 357.613,202.678 356.646,206.742 352.031 C 208.407 350.141,210.278 348.028,210.900 347.335 C 211.522 346.643,212.664 345.373,213.438 344.512 C 214.211 343.651,215.941 341.731,217.282 340.245 C 218.623 338.758,220.029 337.148,220.407 336.665 C 221.032 335.867,223.081 333.572,224.453 332.135 C 224.754 331.820,224.941 331.563,224.868 331.563 C 224.751 331.563,225.925 330.264,227.905 328.203 L 228.581 327.500 200.015 327.500 L 171.450 327.500 172.522 328.457 M267.707 334.297 C 267.364 345.641,267.458 377.680,267.838 379.189 C 268.211 380.671,269.225 381.932,270.626 382.657 C 271.437 383.076,276.518 383.122,313.975 383.050 L 356.406 382.969 357.458 382.218 C 359.727 380.601,359.673 381.137,359.680 360.000 C 359.683 349.602,359.594 338.035,359.481 334.297 L 359.275 327.500 313.594 327.500 L 267.912 327.500 267.707 334.297 " stroke="none" fill="#fcbc4c" fill-rule="evenodd"/></g></svg>
customer_portal/src/assets/images/socfortress_logo_large_dark.svg new
+12
@@ -0,0 +1,12 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!-- Generated by Pixelmator Pro 3.7.1 -->
3 +<svg width="3237" height="497" viewBox="0 0 3237 497" xmlns="http://www.w3.org/2000/svg">
4 + <g id="svgg">
5 + <path id="path0" fill="#fbab44" fill-rule="evenodd" stroke="none" d="M 163.186996 283.316498 C 163.117004 320.059509 163.007004 318.216492 165.423004 320.644501 C 165.964005 321.1875 166.880997 322.204498 167.460999 322.903503 C 168.041 323.603485 168.641998 324.097504 168.796997 324.002502 C 168.951996 323.906494 169.044998 323.944489 169.003998 324.086487 C 168.876999 324.528503 169.815994 325.750488 170.283997 325.750488 C 170.529007 325.750488 170.634995 325.844513 170.520004 325.959503 C 170.404999 326.073486 170.544006 326.425507 170.828995 326.740509 C 171.324005 327.286499 172.623001 327.3125 199.951996 327.3125 L 228.557007 327.3125 L 229.669006 326.111511 C 237.848007 317.277496 236.850006 323.213501 236.796005 283.718506 C 236.770996 265.500488 236.701996 251.613495 236.643005 252.859497 L 236.535004 255.125504 L 200 255.125504 L 163.464996 255.125504 L 163.356995 252.859497 C 163.298004 251.613495 163.220993 265.318512 163.186996 283.316498 M 0.313 265.28949 C 0.313 275.022491 0.341 275.519501 0.973 276.778503 C 2.064 278.949493 3.126 280.146515 4.489 280.742493 C 5.2 281.05249 7.75 282.251495 10.156 283.406494 C 12.563 284.561493 16.289 286.350494 18.438 287.3815 C 20.586 288.412506 23.327999 289.717499 24.531 290.281494 C 25.733999 290.84549 29.18 292.491486 32.188 293.940491 C 35.195 295.388489 38.254002 296.837494 38.984001 297.159485 L 40.313 297.745514 L 40.382 317.138489 C 40.419998 327.804504 40.493999 334.457489 40.546001 331.921509 L 40.641998 327.3125 L 86.405998 327.3125 L 132.171005 327.3125 L 132.278 331.921509 C 132.337006 334.457489 132.363998 323.3125 132.337997 307.156494 C 132.296005 281.639496 132.223007 277.39151 131.783005 274.8125 C 130.884003 269.550507 130.031998 267.139496 127.695 263.250488 C 126.364998 261.037506 123.870003 258.250488 123.220001 258.250488 C 122.996002 258.250488 122.813004 258.125488 122.813004 257.973511 C 122.811996 257.820496 122.355003 257.465515 121.796997 257.183502 C 120.338997 256.446503 119.564003 255.951508 119.307999 255.593506 C 119.161003 255.387497 98.838997 255.253494 59.699001 255.199493 L 0.313 255.117493 L 0.313 265.28949 M 280.703003 255.5495 C 280.31601 255.741501 280 256.005493 280 256.136505 C 280 256.267487 279.812012 256.375488 279.582001 256.375488 C 278.920013 256.375488 275.625 258.861511 275.625 259.360504 C 275.625 259.609497 275.437988 259.8125 275.209015 259.8125 C 274.444 259.8125 272.173004 262.953491 270.915985 265.750488 C 270.567993 266.523499 270.165985 267.374512 270.020996 267.64151 C 269.667999 268.295502 268.526001 272.922485 268.096985 275.4375 C 267.864014 276.804504 267.727997 287.1315 267.682007 307.000488 C 267.644012 323.242493 267.661011 334.457489 267.721008 331.921509 L 267.82901 327.3125 L 313.593994 327.3125 L 359.358002 327.3125 L 359.45401 331.921509 C 359.506012 334.457489 359.582001 327.8125 359.622009 317.156494 L 359.695007 297.781494 L 360.863007 297.235504 C 363.847992 295.842499 370.954987 292.462494 375.739014 290.160492 C 378.638 288.76651 381.044006 287.625488 381.087006 287.625488 C 381.131012 287.625488 384.102997 286.202515 387.691986 284.464508 C 391.282013 282.726501 394.921997 280.990509 395.781006 280.606506 C 399.748993 278.834503 399.868988 278.37149 399.756989 265.257507 L 399.67099 255.125504 L 340.539001 255.163498 C 297.770996 255.191498 281.212006 255.298492 280.703003 255.5495 M 172.720993 328.836487 C 173.440994 329.528503 173.925995 329.724487 173.595001 329.189514 C 173.488998 329.018494 173.093994 328.719513 172.716995 328.525513 L 172.031006 328.172485 L 172.720993 328.836487"/>
6 + <path id="path1" fill="#fb9b3c" fill-rule="evenodd" stroke="none" d="M 0.207 176.296509 C 0.094 180.035522 0.034 203.906494 0.074 229.343506 C 0.115 254.781494 0.186 270.987488 0.234 265.357513 L 0.321 255.121506 L 59.223 255.201508 C 91.619003 255.245499 118.125 255.247498 118.125 255.207504 C 118.125 255.063507 115.844002 254.182495 115.141998 254.055496 C 114.75 253.984497 114.141998 253.772507 113.790001 253.583496 C 113.438004 253.395493 113.039001 253.260498 112.903 253.282501 C 112.766998 253.305496 112.585999 253.296494 112.5 253.263504 C 111.792 252.987503 104.237 251.681503 103.432999 251.696503 C 103.005997 251.704498 101.953003 251.629501 101.094002 251.529495 C 99.332001 251.324493 96.400002 251.037506 94.530998 250.888504 C 93.844002 250.833496 92.508003 250.704498 91.563004 250.601501 C 90.616997 250.498505 89.280998 250.354507 88.594002 250.281494 C 68.487 248.145493 56.970001 240.616516 48.921001 224.343506 C 42.866001 212.102478 38.598 191.004517 38.317001 171.921509 L 38.280998 169.500488 L 19.347 169.500488 L 0.412 169.500488 L 0.207 176.296509 M 39.844002 169.8125 C 39.950001 169.984497 40.240002 170.125488 40.487 170.125488 C 40.735001 170.125488 40.938 170.230499 40.938999 170.359497 C 40.942001 170.701508 42.824001 171.717499 43.117001 171.536499 C 43.254002 171.451508 43.540001 171.591492 43.752998 171.847504 C 43.965 172.103516 44.262001 172.3125 44.412998 172.3125 C 44.563999 172.3125 44.969002 172.593506 45.313 172.9375 C 45.655998 173.281494 46.212002 173.5625 46.547001 173.5625 C 46.882 173.5625 47.269001 173.752502 47.405998 173.985504 C 47.543999 174.217499 48.310001 174.769501 49.110001 175.211487 C 49.909 175.653503 51.136002 176.377502 51.838001 176.820496 C 52.539001 177.262512 53.188999 177.625488 53.282001 177.625488 C 53.375 177.625488 53.974998 177.959503 54.616001 178.3685 C 55.257 178.776489 58.172001 180.481506 61.094002 182.157501 C 64.015999 183.83252 68.023003 186.150513 70 187.307495 C 74.084999 189.698486 97.435997 203.186493 124.375 218.714478 C 147.524002 232.058502 148.697998 232.736511 151.718994 234.516479 C 153.093994 235.326508 154.789993 236.276489 155.488998 236.627502 C 160.132004 238.961487 162.895004 244.114502 163.298004 251.192505 L 163.522003 255.125504 L 199.996002 255.125504 L 236.470993 255.125504 L 236.688004 251.453506 C 237.235992 242.167496 238.811005 239.973511 249.253006 233.958496 C 264.240997 225.323486 277.316986 217.770508 288.906006 211.051514 C 290.993988 209.841492 296.178009 206.860504 303.281006 202.785522 C 307.406006 200.419495 312.39801 197.532501 314.375 196.371521 C 316.35199 195.21051 318.460999 193.980499 319.062988 193.638489 C 319.664001 193.296509 322.89801 191.430481 326.25 189.491516 C 329.60199 187.553497 332.765991 185.728516 333.281006 185.4375 C 334.295013 184.864502 341.338989 180.77948 343.125 179.728516 C 343.72699 179.375488 345.97699 178.047485 348.125 176.77948 C 350.27301 175.510498 352.312988 174.307495 352.656006 174.104492 C 353 173.902496 353.80899 173.41449 354.453003 173.020508 C 355.097992 172.626495 355.625 172.402496 355.625 172.522522 C 355.625 172.643494 356.029999 172.354492 356.524994 171.880493 C 357.311005 171.126495 357.854004 170.867493 358.846985 170.773499 C 358.985992 170.760498 359.013 170.610504 358.90799 170.441498 C 358.799011 170.263489 358.924011 170.211487 359.203003 170.318481 C 359.468994 170.420502 359.687988 170.359497 359.687988 170.181519 C 359.687988 170.003479 359.93399 169.794495 360.234009 169.715515 C 360.535004 169.636505 288.527008 169.556519 200.216003 169.536499 C 93.242996 169.512512 39.715 169.604492 39.844002 169.8125 M 361.723999 171.766479 C 361.726013 173.012512 361.636993 174.875488 361.524994 175.906494 C 361.412994 176.9375 361.234009 179.148499 361.127014 180.818481 C 358.825012 216.876495 347.460999 239.394501 327.963013 246.530502 C 325.065002 247.591507 324.283997 247.865494 323.910004 247.955505 C 323.829987 247.974503 323.02301 248.195496 322.115997 248.445496 C 321.209991 248.696503 320.117004 248.954498 319.687988 249.020493 C 319.257996 249.085495 317.640991 249.350494 316.093994 249.609497 C 313.333008 250.070496 311.391998 250.3255 308.281006 250.634506 C 306.744995 250.787506 306.251007 250.829498 302.187988 251.1465 C 300.247986 251.297501 299.356995 251.390503 295.576996 251.833496 C 293.281006 252.1035 288.384003 252.943497 287.812988 253.166504 C 287.554993 253.266495 287.167999 253.320496 286.953003 253.285507 C 286.738007 253.250504 286.562988 253.384506 286.562988 253.582504 C 286.562988 253.780502 286.36499 253.866501 286.123993 253.773499 C 285.882996 253.681503 285.597992 253.693497 285.48999 253.801498 C 285.382996 253.9095 285.122986 254.028503 284.912994 254.066498 C 279.485992 255.056503 280.799988 255.080505 340.855011 255.102493 L 399.678986 255.125504 L 399.765991 265.203491 C 399.813995 270.74649 399.885986 254.539505 399.925995 229.1875 C 399.966003 203.83551 399.906006 180.035522 399.792999 176.296509 L 399.588013 169.500488 L 380.653015 169.500488 L 361.718994 169.501495 L 361.723999 171.766479"/>
7 + <path id="path2" fill="#fccf5b" fill-rule="evenodd" stroke="none" d="M 43.050999 417.8255 C 42.581001 418.0755 41.807999 418.78949 41.333 419.412506 L 40.469002 420.544495 L 40.387001 456.510498 L 40.305 492.476501 L 41.012001 493.860504 C 41.401001 494.62149 42.116001 495.534485 42.601002 495.887512 L 43.483002 496.531494 L 199.723999 496.610504 C 338.186005 496.680511 356.067993 496.635498 356.872986 496.22049 C 357.372009 495.961487 358.174011 495.234497 358.656006 494.602509 L 359.531006 493.454498 L 359.531006 457.024506 L 359.531006 420.593506 L 358.631012 419.417511 C 356.921997 417.183502 358.256989 417.290497 333.365997 417.384491 C 308.06601 417.479492 309.971008 417.27951 308.593994 419.988495 C 307.992004 421.172485 307.964996 421.929504 307.881012 440.203491 L 307.794006 459.1875 L 289.059998 459.1875 L 270.325989 459.1875 L 270.240997 439.726501 L 270.156006 420.265503 L 269.428009 419.33551 C 267.785004 417.238495 269.036987 417.340515 244.755997 417.3255 C 220.973007 417.311493 221.546005 417.272491 220.063004 419.010498 C 218.772995 420.522491 218.75 420.889496 218.75 440.301514 L 218.75 459.1875 L 200 459.1875 L 181.25 459.1875 L 181.25 440.319489 C 181.25 418.561493 181.296005 418.884491 178.026993 417.645508 C 176.227005 416.964508 133.830002 417.219513 132.283997 417.920502 C 129.591995 419.143494 129.688004 418.3255 129.688004 440.112488 L 129.688004 459.1875 L 110.938004 459.1875 L 92.188004 459.1875 L 92.188004 440.785492 C 92.188004 420.056488 92.175003 419.9245 90.045998 418.301514 L 88.955002 417.468506 L 66.431 417.420502 C 47.275002 417.379486 43.778 417.440491 43.050999 417.8255"/>
8 + <path id="path3" fill="#fc862e" fill-rule="evenodd" stroke="none" d="M 2.859 0.652496 C 2.283 0.9375 1.439 1.734497 0.984 2.422485 L 0.156 3.673492 L 0.146 91.195496 C 0.14 139.333496 0.179 176.646484 0.232 174.114502 L 0.329 169.509521 L 19.461 169.426514 C 29.983999 169.3815 38.759998 169.41449 38.962002 169.500488 C 39.165001 169.58551 111.797997 169.58551 200.367996 169.500488 C 288.938995 169.41449 370.015991 169.3815 380.539001 169.426514 L 399.67099 169.509521 L 399.768005 174.114502 C 399.821014 176.646484 399.859985 139.333496 399.854004 91.195496 L 399.843994 3.673492 L 399.011993 2.416504 C 397.377014 -0.054504 399.98999 0.106506 363.069 0.198486 L 330.156006 0.281494 L 329.104004 1.031494 C 326.75 2.71051 326.890015 0.46051 326.881989 36.921509 L 326.875 69.500488 L 308.906006 69.500488 L 290.937988 69.500488 L 290.937988 44.25351 L 290.937012 19.007507 L 290.234009 17.614502 C 289.772003 16.698486 289.102997 15.981506 288.281006 15.519501 L 287.031006 14.818512 L 254.162994 14.893494 C 217.136993 14.978516 220.574997 14.7435 218.828003 17.309509 L 218.125 18.342499 L 218.125 43.921509 L 218.125 69.500488 L 200 69.500488 L 181.875 69.500488 L 181.875 43.921509 L 181.875 18.342499 L 181.171997 17.309509 C 179.425003 14.7435 182.863007 14.978516 145.837006 14.893494 L 112.969002 14.818512 L 111.719002 15.519501 C 110.897003 15.981506 110.227997 16.698486 109.765999 17.614502 L 109.061996 19.007507 L 109.063004 44.25351 L 109.063004 69.500488 L 91.094002 69.500488 L 73.125 69.500488 L 73.117996 36.921509 C 73.110001 0.46051 73.25 2.71051 70.896004 1.031494 L 69.844002 0.281494 L 36.875 0.207489 C 6.708 0.140503 3.817 0.178497 2.859 0.652496 M 361.64801 171.0625 C 361.64801 171.83551 361.709015 172.152496 361.78299 171.765503 C 361.858002 171.378479 361.858002 170.746521 361.78299 170.359497 C 361.709015 169.972504 361.64801 170.28949 361.64801 171.0625 M 357.144012 171.380493 L 356.406006 172.010498 L 357.265991 171.642517 C 357.738007 171.440491 358.125 171.156494 358.125 171.012512 C 358.125 170.622498 357.958008 170.685486 357.144012 171.380493 M 43.125 171.571503 C 43.125 171.733521 43.547001 172.114502 44.063 172.419495 C 44.580002 172.724487 45 172.824493 45 172.642517 C 45 172.461487 44.834999 172.3125 44.633999 172.3125 C 44.432999 172.3125 44.011002 172.079498 43.695999 171.795502 C 43.382 171.510498 43.125 171.410522 43.125 171.571503"/>
9 + <path id="path4" fill="#fcbc4c" fill-rule="evenodd" stroke="none" d="M 40.519001 334.109497 C 40.405998 337.847504 40.317001 349.41449 40.32 359.8125 C 40.327 380.949493 40.272999 380.413513 42.542 382.030487 L 43.594002 382.781494 L 86.025002 382.862488 C 123.482002 382.934509 128.563004 382.888489 129.373993 382.469513 C 130.774994 381.744507 131.789001 380.48349 132.162003 379.001495 C 132.542007 377.492493 132.636002 345.453491 132.292999 334.109497 L 132.087997 327.3125 L 86.405998 327.3125 L 40.724998 327.3125 L 40.519001 334.109497 M 172.522003 328.269501 C 173.110992 328.795502 173.593994 329.337494 173.593994 329.473511 C 173.593994 329.777496 174.623993 330.96051 183.281006 340.59549 C 185 342.508514 187.25 345.036499 188.281006 346.213501 C 189.313004 347.389496 190.302002 348.504486 190.481003 348.691498 C 190.660004 348.87851 191.919998 350.296509 193.281006 351.843506 C 196.328995 355.307495 196.772995 355.719513 198.037003 356.258514 C 200.772003 357.425507 202.677994 356.458496 206.742004 351.843506 C 208.406998 349.953491 210.278 347.840515 210.899994 347.147491 C 211.522003 346.455505 212.664001 345.185486 213.438004 344.324493 C 214.210999 343.463501 215.940994 341.543488 217.281998 340.057495 C 218.623001 338.570496 220.029007 336.96051 220.406998 336.477509 C 221.031998 335.679504 223.080994 333.384491 224.453003 331.94751 C 224.753998 331.632507 224.940994 331.375488 224.867996 331.375488 C 224.751007 331.375488 225.925003 330.076508 227.904999 328.015503 L 228.580994 327.3125 L 200.014999 327.3125 L 171.449997 327.3125 L 172.522003 328.269501 M 267.707001 334.109497 C 267.364014 345.453491 267.458008 377.492493 267.838013 379.001495 C 268.210999 380.48349 269.225006 381.744507 270.626007 382.469513 C 271.437012 382.888489 276.518005 382.934509 313.975006 382.862488 L 356.406006 382.781494 L 357.458008 382.030487 C 359.72699 380.413513 359.673004 380.949493 359.679993 359.8125 C 359.683014 349.41449 359.593994 337.847504 359.480988 334.109497 L 359.274994 327.3125 L 313.593994 327.3125 L 267.911987 327.3125 L 267.707001 334.109497"/>
10 + </g>
11 + <text id="SOCFortress" xml:space="preserve" x="639" y="374" font-family="Orbitron" font-size="364" fill="#ffffff" letter-spacing="-7.28">SOCFortress</text>
12 +</svg>
customer_portal/src/assets/images/socfortress_logo_large_light.svg new
+12
@@ -0,0 +1,12 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<!-- Generated by Pixelmator Pro 3.7.1 -->
3 +<svg width="3237" height="497" viewBox="0 0 3237 497" xmlns="http://www.w3.org/2000/svg">
4 + <g id="svgg">
5 + <path id="path0" fill="#fbab44" fill-rule="evenodd" stroke="none" d="M 163.186996 283.316498 C 163.117004 320.059509 163.007004 318.216492 165.423004 320.644501 C 165.964005 321.1875 166.880997 322.204498 167.460999 322.903503 C 168.041 323.603485 168.641998 324.097504 168.796997 324.002502 C 168.951996 323.906494 169.044998 323.944489 169.003998 324.086487 C 168.876999 324.528503 169.815994 325.750488 170.283997 325.750488 C 170.529007 325.750488 170.634995 325.844513 170.520004 325.959503 C 170.404999 326.073486 170.544006 326.425507 170.828995 326.740509 C 171.324005 327.286499 172.623001 327.3125 199.951996 327.3125 L 228.557007 327.3125 L 229.669006 326.111511 C 237.848007 317.277496 236.850006 323.213501 236.796005 283.718506 C 236.770996 265.500488 236.701996 251.613495 236.643005 252.859497 L 236.535004 255.125504 L 200 255.125504 L 163.464996 255.125504 L 163.356995 252.859497 C 163.298004 251.613495 163.220993 265.318512 163.186996 283.316498 M 0.313 265.28949 C 0.313 275.022491 0.341 275.519501 0.973 276.778503 C 2.064 278.949493 3.126 280.146515 4.489 280.742493 C 5.2 281.05249 7.75 282.251495 10.156 283.406494 C 12.563 284.561493 16.289 286.350494 18.438 287.3815 C 20.586 288.412506 23.327999 289.717499 24.531 290.281494 C 25.733999 290.84549 29.18 292.491486 32.188 293.940491 C 35.195 295.388489 38.254002 296.837494 38.984001 297.159485 L 40.313 297.745514 L 40.382 317.138489 C 40.419998 327.804504 40.493999 334.457489 40.546001 331.921509 L 40.641998 327.3125 L 86.405998 327.3125 L 132.171005 327.3125 L 132.278 331.921509 C 132.337006 334.457489 132.363998 323.3125 132.337997 307.156494 C 132.296005 281.639496 132.223007 277.39151 131.783005 274.8125 C 130.884003 269.550507 130.031998 267.139496 127.695 263.250488 C 126.364998 261.037506 123.870003 258.250488 123.220001 258.250488 C 122.996002 258.250488 122.813004 258.125488 122.813004 257.973511 C 122.811996 257.820496 122.355003 257.465515 121.796997 257.183502 C 120.338997 256.446503 119.564003 255.951508 119.307999 255.593506 C 119.161003 255.387497 98.838997 255.253494 59.699001 255.199493 L 0.313 255.117493 L 0.313 265.28949 M 280.703003 255.5495 C 280.31601 255.741501 280 256.005493 280 256.136505 C 280 256.267487 279.812012 256.375488 279.582001 256.375488 C 278.920013 256.375488 275.625 258.861511 275.625 259.360504 C 275.625 259.609497 275.437988 259.8125 275.209015 259.8125 C 274.444 259.8125 272.173004 262.953491 270.915985 265.750488 C 270.567993 266.523499 270.165985 267.374512 270.020996 267.64151 C 269.667999 268.295502 268.526001 272.922485 268.096985 275.4375 C 267.864014 276.804504 267.727997 287.1315 267.682007 307.000488 C 267.644012 323.242493 267.661011 334.457489 267.721008 331.921509 L 267.82901 327.3125 L 313.593994 327.3125 L 359.358002 327.3125 L 359.45401 331.921509 C 359.506012 334.457489 359.582001 327.8125 359.622009 317.156494 L 359.695007 297.781494 L 360.863007 297.235504 C 363.847992 295.842499 370.954987 292.462494 375.739014 290.160492 C 378.638 288.76651 381.044006 287.625488 381.087006 287.625488 C 381.131012 287.625488 384.102997 286.202515 387.691986 284.464508 C 391.282013 282.726501 394.921997 280.990509 395.781006 280.606506 C 399.748993 278.834503 399.868988 278.37149 399.756989 265.257507 L 399.67099 255.125504 L 340.539001 255.163498 C 297.770996 255.191498 281.212006 255.298492 280.703003 255.5495 M 172.720993 328.836487 C 173.440994 329.528503 173.925995 329.724487 173.595001 329.189514 C 173.488998 329.018494 173.093994 328.719513 172.716995 328.525513 L 172.031006 328.172485 L 172.720993 328.836487"/>
6 + <path id="path1" fill="#fb9b3c" fill-rule="evenodd" stroke="none" d="M 0.207 176.296509 C 0.094 180.035522 0.034 203.906494 0.074 229.343506 C 0.115 254.781494 0.186 270.987488 0.234 265.357513 L 0.321 255.121506 L 59.223 255.201508 C 91.619003 255.245499 118.125 255.247498 118.125 255.207504 C 118.125 255.063507 115.844002 254.182495 115.141998 254.055496 C 114.75 253.984497 114.141998 253.772507 113.790001 253.583496 C 113.438004 253.395493 113.039001 253.260498 112.903 253.282501 C 112.766998 253.305496 112.585999 253.296494 112.5 253.263504 C 111.792 252.987503 104.237 251.681503 103.432999 251.696503 C 103.005997 251.704498 101.953003 251.629501 101.094002 251.529495 C 99.332001 251.324493 96.400002 251.037506 94.530998 250.888504 C 93.844002 250.833496 92.508003 250.704498 91.563004 250.601501 C 90.616997 250.498505 89.280998 250.354507 88.594002 250.281494 C 68.487 248.145493 56.970001 240.616516 48.921001 224.343506 C 42.866001 212.102478 38.598 191.004517 38.317001 171.921509 L 38.280998 169.500488 L 19.347 169.500488 L 0.412 169.500488 L 0.207 176.296509 M 39.844002 169.8125 C 39.950001 169.984497 40.240002 170.125488 40.487 170.125488 C 40.735001 170.125488 40.938 170.230499 40.938999 170.359497 C 40.942001 170.701508 42.824001 171.717499 43.117001 171.536499 C 43.254002 171.451508 43.540001 171.591492 43.752998 171.847504 C 43.965 172.103516 44.262001 172.3125 44.412998 172.3125 C 44.563999 172.3125 44.969002 172.593506 45.313 172.9375 C 45.655998 173.281494 46.212002 173.5625 46.547001 173.5625 C 46.882 173.5625 47.269001 173.752502 47.405998 173.985504 C 47.543999 174.217499 48.310001 174.769501 49.110001 175.211487 C 49.909 175.653503 51.136002 176.377502 51.838001 176.820496 C 52.539001 177.262512 53.188999 177.625488 53.282001 177.625488 C 53.375 177.625488 53.974998 177.959503 54.616001 178.3685 C 55.257 178.776489 58.172001 180.481506 61.094002 182.157501 C 64.015999 183.83252 68.023003 186.150513 70 187.307495 C 74.084999 189.698486 97.435997 203.186493 124.375 218.714478 C 147.524002 232.058502 148.697998 232.736511 151.718994 234.516479 C 153.093994 235.326508 154.789993 236.276489 155.488998 236.627502 C 160.132004 238.961487 162.895004 244.114502 163.298004 251.192505 L 163.522003 255.125504 L 199.996002 255.125504 L 236.470993 255.125504 L 236.688004 251.453506 C 237.235992 242.167496 238.811005 239.973511 249.253006 233.958496 C 264.240997 225.323486 277.316986 217.770508 288.906006 211.051514 C 290.993988 209.841492 296.178009 206.860504 303.281006 202.785522 C 307.406006 200.419495 312.39801 197.532501 314.375 196.371521 C 316.35199 195.21051 318.460999 193.980499 319.062988 193.638489 C 319.664001 193.296509 322.89801 191.430481 326.25 189.491516 C 329.60199 187.553497 332.765991 185.728516 333.281006 185.4375 C 334.295013 184.864502 341.338989 180.77948 343.125 179.728516 C 343.72699 179.375488 345.97699 178.047485 348.125 176.77948 C 350.27301 175.510498 352.312988 174.307495 352.656006 174.104492 C 353 173.902496 353.80899 173.41449 354.453003 173.020508 C 355.097992 172.626495 355.625 172.402496 355.625 172.522522 C 355.625 172.643494 356.029999 172.354492 356.524994 171.880493 C 357.311005 171.126495 357.854004 170.867493 358.846985 170.773499 C 358.985992 170.760498 359.013 170.610504 358.90799 170.441498 C 358.799011 170.263489 358.924011 170.211487 359.203003 170.318481 C 359.468994 170.420502 359.687988 170.359497 359.687988 170.181519 C 359.687988 170.003479 359.93399 169.794495 360.234009 169.715515 C 360.535004 169.636505 288.527008 169.556519 200.216003 169.536499 C 93.242996 169.512512 39.715 169.604492 39.844002 169.8125 M 361.723999 171.766479 C 361.726013 173.012512 361.636993 174.875488 361.524994 175.906494 C 361.412994 176.9375 361.234009 179.148499 361.127014 180.818481 C 358.825012 216.876495 347.460999 239.394501 327.963013 246.530502 C 325.065002 247.591507 324.283997 247.865494 323.910004 247.955505 C 323.829987 247.974503 323.02301 248.195496 322.115997 248.445496 C 321.209991 248.696503 320.117004 248.954498 319.687988 249.020493 C 319.257996 249.085495 317.640991 249.350494 316.093994 249.609497 C 313.333008 250.070496 311.391998 250.3255 308.281006 250.634506 C 306.744995 250.787506 306.251007 250.829498 302.187988 251.1465 C 300.247986 251.297501 299.356995 251.390503 295.576996 251.833496 C 293.281006 252.1035 288.384003 252.943497 287.812988 253.166504 C 287.554993 253.266495 287.167999 253.320496 286.953003 253.285507 C 286.738007 253.250504 286.562988 253.384506 286.562988 253.582504 C 286.562988 253.780502 286.36499 253.866501 286.123993 253.773499 C 285.882996 253.681503 285.597992 253.693497 285.48999 253.801498 C 285.382996 253.9095 285.122986 254.028503 284.912994 254.066498 C 279.485992 255.056503 280.799988 255.080505 340.855011 255.102493 L 399.678986 255.125504 L 399.765991 265.203491 C 399.813995 270.74649 399.885986 254.539505 399.925995 229.1875 C 399.966003 203.83551 399.906006 180.035522 399.792999 176.296509 L 399.588013 169.500488 L 380.653015 169.500488 L 361.718994 169.501495 L 361.723999 171.766479"/>
7 + <path id="path2" fill="#fccf5b" fill-rule="evenodd" stroke="none" d="M 43.050999 417.8255 C 42.581001 418.0755 41.807999 418.78949 41.333 419.412506 L 40.469002 420.544495 L 40.387001 456.510498 L 40.305 492.476501 L 41.012001 493.860504 C 41.401001 494.62149 42.116001 495.534485 42.601002 495.887512 L 43.483002 496.531494 L 199.723999 496.610504 C 338.186005 496.680511 356.067993 496.635498 356.872986 496.22049 C 357.372009 495.961487 358.174011 495.234497 358.656006 494.602509 L 359.531006 493.454498 L 359.531006 457.024506 L 359.531006 420.593506 L 358.631012 419.417511 C 356.921997 417.183502 358.256989 417.290497 333.365997 417.384491 C 308.06601 417.479492 309.971008 417.27951 308.593994 419.988495 C 307.992004 421.172485 307.964996 421.929504 307.881012 440.203491 L 307.794006 459.1875 L 289.059998 459.1875 L 270.325989 459.1875 L 270.240997 439.726501 L 270.156006 420.265503 L 269.428009 419.33551 C 267.785004 417.238495 269.036987 417.340515 244.755997 417.3255 C 220.973007 417.311493 221.546005 417.272491 220.063004 419.010498 C 218.772995 420.522491 218.75 420.889496 218.75 440.301514 L 218.75 459.1875 L 200 459.1875 L 181.25 459.1875 L 181.25 440.319489 C 181.25 418.561493 181.296005 418.884491 178.026993 417.645508 C 176.227005 416.964508 133.830002 417.219513 132.283997 417.920502 C 129.591995 419.143494 129.688004 418.3255 129.688004 440.112488 L 129.688004 459.1875 L 110.938004 459.1875 L 92.188004 459.1875 L 92.188004 440.785492 C 92.188004 420.056488 92.175003 419.9245 90.045998 418.301514 L 88.955002 417.468506 L 66.431 417.420502 C 47.275002 417.379486 43.778 417.440491 43.050999 417.8255"/>
8 + <path id="path3" fill="#fc862e" fill-rule="evenodd" stroke="none" d="M 2.859 0.652496 C 2.283 0.9375 1.439 1.734497 0.984 2.422485 L 0.156 3.673492 L 0.146 91.195496 C 0.14 139.333496 0.179 176.646484 0.232 174.114502 L 0.329 169.509521 L 19.461 169.426514 C 29.983999 169.3815 38.759998 169.41449 38.962002 169.500488 C 39.165001 169.58551 111.797997 169.58551 200.367996 169.500488 C 288.938995 169.41449 370.015991 169.3815 380.539001 169.426514 L 399.67099 169.509521 L 399.768005 174.114502 C 399.821014 176.646484 399.859985 139.333496 399.854004 91.195496 L 399.843994 3.673492 L 399.011993 2.416504 C 397.377014 -0.054504 399.98999 0.106506 363.069 0.198486 L 330.156006 0.281494 L 329.104004 1.031494 C 326.75 2.71051 326.890015 0.46051 326.881989 36.921509 L 326.875 69.500488 L 308.906006 69.500488 L 290.937988 69.500488 L 290.937988 44.25351 L 290.937012 19.007507 L 290.234009 17.614502 C 289.772003 16.698486 289.102997 15.981506 288.281006 15.519501 L 287.031006 14.818512 L 254.162994 14.893494 C 217.136993 14.978516 220.574997 14.7435 218.828003 17.309509 L 218.125 18.342499 L 218.125 43.921509 L 218.125 69.500488 L 200 69.500488 L 181.875 69.500488 L 181.875 43.921509 L 181.875 18.342499 L 181.171997 17.309509 C 179.425003 14.7435 182.863007 14.978516 145.837006 14.893494 L 112.969002 14.818512 L 111.719002 15.519501 C 110.897003 15.981506 110.227997 16.698486 109.765999 17.614502 L 109.061996 19.007507 L 109.063004 44.25351 L 109.063004 69.500488 L 91.094002 69.500488 L 73.125 69.500488 L 73.117996 36.921509 C 73.110001 0.46051 73.25 2.71051 70.896004 1.031494 L 69.844002 0.281494 L 36.875 0.207489 C 6.708 0.140503 3.817 0.178497 2.859 0.652496 M 361.64801 171.0625 C 361.64801 171.83551 361.709015 172.152496 361.78299 171.765503 C 361.858002 171.378479 361.858002 170.746521 361.78299 170.359497 C 361.709015 169.972504 361.64801 170.28949 361.64801 171.0625 M 357.144012 171.380493 L 356.406006 172.010498 L 357.265991 171.642517 C 357.738007 171.440491 358.125 171.156494 358.125 171.012512 C 358.125 170.622498 357.958008 170.685486 357.144012 171.380493 M 43.125 171.571503 C 43.125 171.733521 43.547001 172.114502 44.063 172.419495 C 44.580002 172.724487 45 172.824493 45 172.642517 C 45 172.461487 44.834999 172.3125 44.633999 172.3125 C 44.432999 172.3125 44.011002 172.079498 43.695999 171.795502 C 43.382 171.510498 43.125 171.410522 43.125 171.571503"/>
9 + <path id="path4" fill="#fcbc4c" fill-rule="evenodd" stroke="none" d="M 40.519001 334.109497 C 40.405998 337.847504 40.317001 349.41449 40.32 359.8125 C 40.327 380.949493 40.272999 380.413513 42.542 382.030487 L 43.594002 382.781494 L 86.025002 382.862488 C 123.482002 382.934509 128.563004 382.888489 129.373993 382.469513 C 130.774994 381.744507 131.789001 380.48349 132.162003 379.001495 C 132.542007 377.492493 132.636002 345.453491 132.292999 334.109497 L 132.087997 327.3125 L 86.405998 327.3125 L 40.724998 327.3125 L 40.519001 334.109497 M 172.522003 328.269501 C 173.110992 328.795502 173.593994 329.337494 173.593994 329.473511 C 173.593994 329.777496 174.623993 330.96051 183.281006 340.59549 C 185 342.508514 187.25 345.036499 188.281006 346.213501 C 189.313004 347.389496 190.302002 348.504486 190.481003 348.691498 C 190.660004 348.87851 191.919998 350.296509 193.281006 351.843506 C 196.328995 355.307495 196.772995 355.719513 198.037003 356.258514 C 200.772003 357.425507 202.677994 356.458496 206.742004 351.843506 C 208.406998 349.953491 210.278 347.840515 210.899994 347.147491 C 211.522003 346.455505 212.664001 345.185486 213.438004 344.324493 C 214.210999 343.463501 215.940994 341.543488 217.281998 340.057495 C 218.623001 338.570496 220.029007 336.96051 220.406998 336.477509 C 221.031998 335.679504 223.080994 333.384491 224.453003 331.94751 C 224.753998 331.632507 224.940994 331.375488 224.867996 331.375488 C 224.751007 331.375488 225.925003 330.076508 227.904999 328.015503 L 228.580994 327.3125 L 200.014999 327.3125 L 171.449997 327.3125 L 172.522003 328.269501 M 267.707001 334.109497 C 267.364014 345.453491 267.458008 377.492493 267.838013 379.001495 C 268.210999 380.48349 269.225006 381.744507 270.626007 382.469513 C 271.437012 382.888489 276.518005 382.934509 313.975006 382.862488 L 356.406006 382.781494 L 357.458008 382.030487 C 359.72699 380.413513 359.673004 380.949493 359.679993 359.8125 C 359.683014 349.41449 359.593994 337.847504 359.480988 334.109497 L 359.274994 327.3125 L 313.593994 327.3125 L 267.911987 327.3125 L 267.707001 334.109497"/>
10 + </g>
11 + <text id="SOCFortress" xml:space="preserve"><tspan x="637" y="376" font-family="Orbitron" font-size="364" fill="#000000" letter-spacing="-7.28" xml:space="preserve">SOCF</tspan><tspan font-family="Orbitron" font-size="364" fill="#000000" letter-spacing="-7.28" xml:space="preserve">or</tspan><tspan font-family="Orbitron" font-size="364" fill="#000000" letter-spacing="-7.28" xml:space="preserve">tr</tspan><tspan font-family="Orbitron" font-size="364" fill="#000000" letter-spacing="-7.28" xml:space="preserve">es</tspan><tspan font-family="Orbitron" font-size="364" fill="#000000" letter-spacing="-7.28" xml:space="preserve">s</tspan></text>
12 +</svg>
customer_portal/src/assets/images/socfortress_logo_small_dark.svg new
+3
@@ -0,0 +1,3 @@
1 +<?xml version="1.0" standalone="no"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg" version="1.1" width="400" height="497.1875" viewBox="0, 0, 400,497.1875"><g id="svgg"><path id="path0" d="M163.187 283.504 C 163.117 320.247,163.007 318.404,165.423 320.832 C 165.964 321.375,166.881 322.392,167.461 323.091 C 168.041 323.791,168.642 324.285,168.797 324.190 C 168.952 324.094,169.045 324.132,169.004 324.274 C 168.877 324.716,169.816 325.938,170.284 325.938 C 170.529 325.938,170.635 326.032,170.520 326.147 C 170.405 326.261,170.544 326.613,170.829 326.928 C 171.324 327.474,172.623 327.500,199.952 327.500 L 228.557 327.500 229.669 326.299 C 237.848 317.465,236.850 323.401,236.796 283.906 C 236.771 265.688,236.702 251.801,236.643 253.047 L 236.535 255.313 200.000 255.313 L 163.465 255.313 163.357 253.047 C 163.298 251.801,163.221 265.506,163.187 283.504 M0.313 265.477 C 0.313 275.210,0.341 275.707,0.973 276.966 C 2.064 279.137,3.126 280.334,4.489 280.930 C 5.200 281.240,7.750 282.439,10.156 283.594 C 12.563 284.749,16.289 286.538,18.438 287.569 C 20.586 288.600,23.328 289.905,24.531 290.469 C 25.734 291.033,29.180 292.679,32.188 294.128 C 35.195 295.576,38.254 297.025,38.984 297.347 L 40.313 297.933 40.382 317.326 C 40.420 327.992,40.494 334.645,40.546 332.109 L 40.642 327.500 86.406 327.500 L 132.171 327.500 132.278 332.109 C 132.337 334.645,132.364 323.500,132.338 307.344 C 132.296 281.827,132.223 277.579,131.783 275.000 C 130.884 269.738,130.032 267.327,127.695 263.438 C 126.365 261.225,123.870 258.438,123.220 258.438 C 122.996 258.438,122.813 258.313,122.813 258.161 C 122.812 258.008,122.355 257.653,121.797 257.371 C 120.339 256.634,119.564 256.139,119.308 255.781 C 119.161 255.575,98.839 255.441,59.699 255.387 L 0.313 255.305 0.313 265.477 M280.703 255.737 C 280.316 255.929,280.000 256.193,280.000 256.324 C 280.000 256.455,279.812 256.563,279.582 256.563 C 278.920 256.563,275.625 259.049,275.625 259.548 C 275.625 259.797,275.438 260.000,275.209 260.000 C 274.444 260.000,272.173 263.141,270.916 265.938 C 270.568 266.711,270.166 267.562,270.021 267.829 C 269.668 268.483,268.526 273.110,268.097 275.625 C 267.864 276.992,267.728 287.319,267.682 307.188 C 267.644 323.430,267.661 334.645,267.721 332.109 L 267.829 327.500 313.594 327.500 L 359.358 327.500 359.454 332.109 C 359.506 334.645,359.582 328.000,359.622 317.344 L 359.695 297.969 360.863 297.423 C 363.848 296.030,370.955 292.650,375.739 290.348 C 378.638 288.954,381.044 287.813,381.087 287.813 C 381.131 287.813,384.103 286.390,387.692 284.652 C 391.282 282.914,394.922 281.178,395.781 280.794 C 399.749 279.022,399.869 278.559,399.757 265.445 L 399.671 255.313 340.539 255.351 C 297.771 255.379,281.212 255.486,280.703 255.737 M172.721 329.024 C 173.441 329.716,173.926 329.912,173.595 329.377 C 173.489 329.206,173.094 328.907,172.717 328.713 L 172.031 328.360 172.721 329.024 " stroke="none" fill="#fbab44" fill-rule="evenodd"/><path id="path1" d="M0.207 176.484 C 0.094 180.223,0.034 204.094,0.074 229.531 C 0.115 254.969,0.186 271.175,0.234 265.545 L 0.321 255.309 59.223 255.389 C 91.619 255.433,118.125 255.435,118.125 255.395 C 118.125 255.251,115.844 254.370,115.142 254.243 C 114.750 254.172,114.142 253.960,113.790 253.771 C 113.438 253.583,113.039 253.448,112.903 253.470 C 112.767 253.493,112.586 253.484,112.500 253.451 C 111.792 253.175,104.237 251.869,103.433 251.884 C 103.006 251.892,101.953 251.817,101.094 251.717 C 99.332 251.512,96.400 251.225,94.531 251.076 C 93.844 251.021,92.508 250.892,91.563 250.789 C 90.617 250.686,89.281 250.542,88.594 250.469 C 68.487 248.333,56.970 240.804,48.921 224.531 C 42.866 212.290,38.598 191.192,38.317 172.109 L 38.281 169.688 19.347 169.688 L 0.412 169.688 0.207 176.484 M39.844 170.000 C 39.950 170.172,40.240 170.313,40.487 170.313 C 40.735 170.313,40.938 170.418,40.939 170.547 C 40.942 170.889,42.824 171.905,43.117 171.724 C 43.254 171.639,43.540 171.779,43.753 172.035 C 43.965 172.291,44.262 172.500,44.413 172.500 C 44.564 172.500,44.969 172.781,45.313 173.125 C 45.656 173.469,46.212 173.750,46.547 173.750 C 46.882 173.750,47.269 173.940,47.406 174.173 C 47.544 174.405,48.310 174.957,49.110 175.399 C 49.909 175.841,51.136 176.565,51.838 177.008 C 52.539 177.450,53.189 177.813,53.282 177.813 C 53.375 177.813,53.975 178.147,54.616 178.556 C 55.257 178.964,58.172 180.669,61.094 182.345 C 64.016 184.020,68.023 186.338,70.000 187.495 C 74.085 189.886,97.436 203.374,124.375 218.902 C 147.524 232.246,148.698 232.924,151.719 234.704 C 153.094 235.514,154.790 236.464,155.489 236.815 C 160.132 239.149,162.895 244.302,163.298 251.380 L 163.522 255.313 199.996 255.313 L 236.471 255.313 236.688 251.641 C 237.236 242.355,238.811 240.161,249.253 234.146 C 264.241 225.511,277.317 217.958,288.906 211.239 C 290.994 210.029,296.178 207.048,303.281 202.973 C 307.406 200.607,312.398 197.720,314.375 196.559 C 316.352 195.398,318.461 194.168,319.063 193.826 C 319.664 193.484,322.898 191.618,326.250 189.679 C 329.602 187.741,332.766 185.916,333.281 185.625 C 334.295 185.052,341.339 180.967,343.125 179.916 C 343.727 179.563,345.977 178.235,348.125 176.967 C 350.273 175.698,352.313 174.495,352.656 174.292 C 353.000 174.090,353.809 173.602,354.453 173.208 C 355.098 172.814,355.625 172.590,355.625 172.710 C 355.625 172.831,356.030 172.542,356.525 172.068 C 357.311 171.314,357.854 171.055,358.847 170.961 C 358.986 170.948,359.013 170.798,358.908 170.629 C 358.799 170.451,358.924 170.399,359.203 170.506 C 359.469 170.608,359.688 170.547,359.688 170.369 C 359.688 170.191,359.934 169.982,360.234 169.903 C 360.535 169.824,288.527 169.744,200.216 169.724 C 93.243 169.700,39.715 169.792,39.844 170.000 M361.724 171.954 C 361.726 173.200,361.637 175.063,361.525 176.094 C 361.413 177.125,361.234 179.336,361.127 181.006 C 358.825 217.064,347.461 239.582,327.963 246.718 C 325.065 247.779,324.284 248.053,323.910 248.143 C 323.830 248.162,323.023 248.383,322.116 248.633 C 321.210 248.884,320.117 249.142,319.688 249.208 C 319.258 249.273,317.641 249.538,316.094 249.797 C 313.333 250.258,311.392 250.513,308.281 250.822 C 306.745 250.975,306.251 251.017,302.188 251.334 C 300.248 251.485,299.357 251.578,295.577 252.021 C 293.281 252.291,288.384 253.131,287.813 253.354 C 287.555 253.454,287.168 253.508,286.953 253.473 C 286.738 253.438,286.563 253.572,286.563 253.770 C 286.563 253.968,286.365 254.054,286.124 253.961 C 285.883 253.869,285.598 253.881,285.490 253.989 C 285.383 254.097,285.123 254.216,284.913 254.254 C 279.486 255.244,280.800 255.268,340.855 255.290 L 399.679 255.313 399.766 265.391 C 399.814 270.934,399.886 254.727,399.926 229.375 C 399.966 204.023,399.906 180.223,399.793 176.484 L 399.588 169.688 380.653 169.688 L 361.719 169.689 361.724 171.954 " stroke="none" fill="#fb9b3c" fill-rule="evenodd"/><path id="path2" d="M43.051 418.013 C 42.581 418.263,41.808 418.977,41.333 419.600 L 40.469 420.732 40.387 456.698 L 40.305 492.664 41.012 494.048 C 41.401 494.809,42.116 495.722,42.601 496.075 L 43.483 496.719 199.724 496.798 C 338.186 496.868,356.068 496.823,356.873 496.408 C 357.372 496.149,358.174 495.422,358.656 494.790 L 359.531 493.642 359.531 457.212 L 359.531 420.781 358.631 419.605 C 356.922 417.371,358.257 417.478,333.366 417.572 C 308.066 417.667,309.971 417.467,308.594 420.176 C 307.992 421.360,307.965 422.117,307.881 440.391 L 307.794 459.375 289.060 459.375 L 270.326 459.375 270.241 439.914 L 270.156 420.453 269.428 419.523 C 267.785 417.426,269.037 417.528,244.756 417.513 C 220.973 417.499,221.546 417.460,220.063 419.198 C 218.773 420.710,218.750 421.077,218.750 440.489 L 218.750 459.375 200.000 459.375 L 181.250 459.375 181.250 440.507 C 181.250 418.749,181.296 419.072,178.027 417.833 C 176.227 417.152,133.830 417.407,132.284 418.108 C 129.592 419.331,129.688 418.513,129.688 440.300 L 129.688 459.375 110.938 459.375 L 92.188 459.375 92.188 440.973 C 92.188 420.244,92.175 420.112,90.046 418.489 L 88.955 417.656 66.431 417.608 C 47.275 417.567,43.778 417.628,43.051 418.013 " stroke="none" fill="#fccf5b" fill-rule="evenodd"/><path id="path3" d="M2.859 0.840 C 2.283 1.125,1.439 1.922,0.984 2.610 L 0.156 3.861 0.146 91.383 C 0.140 139.521,0.179 176.834,0.232 174.302 L 0.329 169.697 19.461 169.614 C 29.984 169.569,38.760 169.602,38.962 169.688 C 39.165 169.773,111.798 169.773,200.368 169.688 C 288.939 169.602,370.016 169.569,380.539 169.614 L 399.671 169.697 399.768 174.302 C 399.821 176.834,399.860 139.521,399.854 91.383 L 399.844 3.861 399.012 2.604 C 397.377 0.133,399.990 0.294,363.069 0.386 L 330.156 0.469 329.104 1.219 C 326.750 2.898,326.890 0.648,326.882 37.109 L 326.875 69.688 308.906 69.688 L 290.938 69.688 290.938 44.441 L 290.937 19.195 290.234 17.802 C 289.772 16.886,289.103 16.169,288.281 15.707 L 287.031 15.006 254.163 15.081 C 217.137 15.166,220.575 14.931,218.828 17.497 L 218.125 18.530 218.125 44.109 L 218.125 69.688 200.000 69.688 L 181.875 69.688 181.875 44.109 L 181.875 18.530 181.172 17.497 C 179.425 14.931,182.863 15.166,145.837 15.081 L 112.969 15.006 111.719 15.707 C 110.897 16.169,110.228 16.886,109.766 17.802 L 109.062 19.195 109.063 44.441 L 109.063 69.688 91.094 69.688 L 73.125 69.688 73.118 37.109 C 73.110 0.648,73.250 2.898,70.896 1.219 L 69.844 0.469 36.875 0.395 C 6.708 0.328,3.817 0.366,2.859 0.840 M361.648 171.250 C 361.648 172.023,361.709 172.340,361.783 171.953 C 361.858 171.566,361.858 170.934,361.783 170.547 C 361.709 170.160,361.648 170.477,361.648 171.250 M357.144 171.568 L 356.406 172.198 357.266 171.830 C 357.738 171.628,358.125 171.344,358.125 171.200 C 358.125 170.810,357.958 170.873,357.144 171.568 M43.125 171.759 C 43.125 171.921,43.547 172.302,44.063 172.607 C 44.580 172.912,45.000 173.012,45.000 172.830 C 45.000 172.649,44.835 172.500,44.634 172.500 C 44.433 172.500,44.011 172.267,43.696 171.983 C 43.382 171.698,43.125 171.598,43.125 171.759 " stroke="none" fill="#fc862e" fill-rule="evenodd"/><path id="path4" d="M40.519 334.297 C 40.406 338.035,40.317 349.602,40.320 360.000 C 40.327 381.137,40.273 380.601,42.542 382.218 L 43.594 382.969 86.025 383.050 C 123.482 383.122,128.563 383.076,129.374 382.657 C 130.775 381.932,131.789 380.671,132.162 379.189 C 132.542 377.680,132.636 345.641,132.293 334.297 L 132.088 327.500 86.406 327.500 L 40.725 327.500 40.519 334.297 M172.522 328.457 C 173.111 328.983,173.594 329.525,173.594 329.661 C 173.594 329.965,174.624 331.148,183.281 340.783 C 185.000 342.696,187.250 345.224,188.281 346.401 C 189.313 347.577,190.302 348.692,190.481 348.879 C 190.660 349.066,191.920 350.484,193.281 352.031 C 196.329 355.495,196.773 355.907,198.037 356.446 C 200.772 357.613,202.678 356.646,206.742 352.031 C 208.407 350.141,210.278 348.028,210.900 347.335 C 211.522 346.643,212.664 345.373,213.438 344.512 C 214.211 343.651,215.941 341.731,217.282 340.245 C 218.623 338.758,220.029 337.148,220.407 336.665 C 221.032 335.867,223.081 333.572,224.453 332.135 C 224.754 331.820,224.941 331.563,224.868 331.563 C 224.751 331.563,225.925 330.264,227.905 328.203 L 228.581 327.500 200.015 327.500 L 171.450 327.500 172.522 328.457 M267.707 334.297 C 267.364 345.641,267.458 377.680,267.838 379.189 C 268.211 380.671,269.225 381.932,270.626 382.657 C 271.437 383.076,276.518 383.122,313.975 383.050 L 356.406 382.969 357.458 382.218 C 359.727 380.601,359.673 381.137,359.680 360.000 C 359.683 349.602,359.594 338.035,359.481 334.297 L 359.275 327.500 313.594 327.500 L 267.912 327.500 267.707 334.297 " stroke="none" fill="#fcbc4c" fill-rule="evenodd"/></g></svg>
customer_portal/src/assets/images/socfortress_logo_small_light.svg new
+3
@@ -0,0 +1,3 @@
1 +<?xml version="1.0" standalone="no"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg" version="1.1" width="400" height="497.1875" viewBox="0, 0, 400,497.1875"><g id="svgg"><path id="path0" d="M163.187 283.504 C 163.117 320.247,163.007 318.404,165.423 320.832 C 165.964 321.375,166.881 322.392,167.461 323.091 C 168.041 323.791,168.642 324.285,168.797 324.190 C 168.952 324.094,169.045 324.132,169.004 324.274 C 168.877 324.716,169.816 325.938,170.284 325.938 C 170.529 325.938,170.635 326.032,170.520 326.147 C 170.405 326.261,170.544 326.613,170.829 326.928 C 171.324 327.474,172.623 327.500,199.952 327.500 L 228.557 327.500 229.669 326.299 C 237.848 317.465,236.850 323.401,236.796 283.906 C 236.771 265.688,236.702 251.801,236.643 253.047 L 236.535 255.313 200.000 255.313 L 163.465 255.313 163.357 253.047 C 163.298 251.801,163.221 265.506,163.187 283.504 M0.313 265.477 C 0.313 275.210,0.341 275.707,0.973 276.966 C 2.064 279.137,3.126 280.334,4.489 280.930 C 5.200 281.240,7.750 282.439,10.156 283.594 C 12.563 284.749,16.289 286.538,18.438 287.569 C 20.586 288.600,23.328 289.905,24.531 290.469 C 25.734 291.033,29.180 292.679,32.188 294.128 C 35.195 295.576,38.254 297.025,38.984 297.347 L 40.313 297.933 40.382 317.326 C 40.420 327.992,40.494 334.645,40.546 332.109 L 40.642 327.500 86.406 327.500 L 132.171 327.500 132.278 332.109 C 132.337 334.645,132.364 323.500,132.338 307.344 C 132.296 281.827,132.223 277.579,131.783 275.000 C 130.884 269.738,130.032 267.327,127.695 263.438 C 126.365 261.225,123.870 258.438,123.220 258.438 C 122.996 258.438,122.813 258.313,122.813 258.161 C 122.812 258.008,122.355 257.653,121.797 257.371 C 120.339 256.634,119.564 256.139,119.308 255.781 C 119.161 255.575,98.839 255.441,59.699 255.387 L 0.313 255.305 0.313 265.477 M280.703 255.737 C 280.316 255.929,280.000 256.193,280.000 256.324 C 280.000 256.455,279.812 256.563,279.582 256.563 C 278.920 256.563,275.625 259.049,275.625 259.548 C 275.625 259.797,275.438 260.000,275.209 260.000 C 274.444 260.000,272.173 263.141,270.916 265.938 C 270.568 266.711,270.166 267.562,270.021 267.829 C 269.668 268.483,268.526 273.110,268.097 275.625 C 267.864 276.992,267.728 287.319,267.682 307.188 C 267.644 323.430,267.661 334.645,267.721 332.109 L 267.829 327.500 313.594 327.500 L 359.358 327.500 359.454 332.109 C 359.506 334.645,359.582 328.000,359.622 317.344 L 359.695 297.969 360.863 297.423 C 363.848 296.030,370.955 292.650,375.739 290.348 C 378.638 288.954,381.044 287.813,381.087 287.813 C 381.131 287.813,384.103 286.390,387.692 284.652 C 391.282 282.914,394.922 281.178,395.781 280.794 C 399.749 279.022,399.869 278.559,399.757 265.445 L 399.671 255.313 340.539 255.351 C 297.771 255.379,281.212 255.486,280.703 255.737 M172.721 329.024 C 173.441 329.716,173.926 329.912,173.595 329.377 C 173.489 329.206,173.094 328.907,172.717 328.713 L 172.031 328.360 172.721 329.024 " stroke="none" fill="#fbab44" fill-rule="evenodd"/><path id="path1" d="M0.207 176.484 C 0.094 180.223,0.034 204.094,0.074 229.531 C 0.115 254.969,0.186 271.175,0.234 265.545 L 0.321 255.309 59.223 255.389 C 91.619 255.433,118.125 255.435,118.125 255.395 C 118.125 255.251,115.844 254.370,115.142 254.243 C 114.750 254.172,114.142 253.960,113.790 253.771 C 113.438 253.583,113.039 253.448,112.903 253.470 C 112.767 253.493,112.586 253.484,112.500 253.451 C 111.792 253.175,104.237 251.869,103.433 251.884 C 103.006 251.892,101.953 251.817,101.094 251.717 C 99.332 251.512,96.400 251.225,94.531 251.076 C 93.844 251.021,92.508 250.892,91.563 250.789 C 90.617 250.686,89.281 250.542,88.594 250.469 C 68.487 248.333,56.970 240.804,48.921 224.531 C 42.866 212.290,38.598 191.192,38.317 172.109 L 38.281 169.688 19.347 169.688 L 0.412 169.688 0.207 176.484 M39.844 170.000 C 39.950 170.172,40.240 170.313,40.487 170.313 C 40.735 170.313,40.938 170.418,40.939 170.547 C 40.942 170.889,42.824 171.905,43.117 171.724 C 43.254 171.639,43.540 171.779,43.753 172.035 C 43.965 172.291,44.262 172.500,44.413 172.500 C 44.564 172.500,44.969 172.781,45.313 173.125 C 45.656 173.469,46.212 173.750,46.547 173.750 C 46.882 173.750,47.269 173.940,47.406 174.173 C 47.544 174.405,48.310 174.957,49.110 175.399 C 49.909 175.841,51.136 176.565,51.838 177.008 C 52.539 177.450,53.189 177.813,53.282 177.813 C 53.375 177.813,53.975 178.147,54.616 178.556 C 55.257 178.964,58.172 180.669,61.094 182.345 C 64.016 184.020,68.023 186.338,70.000 187.495 C 74.085 189.886,97.436 203.374,124.375 218.902 C 147.524 232.246,148.698 232.924,151.719 234.704 C 153.094 235.514,154.790 236.464,155.489 236.815 C 160.132 239.149,162.895 244.302,163.298 251.380 L 163.522 255.313 199.996 255.313 L 236.471 255.313 236.688 251.641 C 237.236 242.355,238.811 240.161,249.253 234.146 C 264.241 225.511,277.317 217.958,288.906 211.239 C 290.994 210.029,296.178 207.048,303.281 202.973 C 307.406 200.607,312.398 197.720,314.375 196.559 C 316.352 195.398,318.461 194.168,319.063 193.826 C 319.664 193.484,322.898 191.618,326.250 189.679 C 329.602 187.741,332.766 185.916,333.281 185.625 C 334.295 185.052,341.339 180.967,343.125 179.916 C 343.727 179.563,345.977 178.235,348.125 176.967 C 350.273 175.698,352.313 174.495,352.656 174.292 C 353.000 174.090,353.809 173.602,354.453 173.208 C 355.098 172.814,355.625 172.590,355.625 172.710 C 355.625 172.831,356.030 172.542,356.525 172.068 C 357.311 171.314,357.854 171.055,358.847 170.961 C 358.986 170.948,359.013 170.798,358.908 170.629 C 358.799 170.451,358.924 170.399,359.203 170.506 C 359.469 170.608,359.688 170.547,359.688 170.369 C 359.688 170.191,359.934 169.982,360.234 169.903 C 360.535 169.824,288.527 169.744,200.216 169.724 C 93.243 169.700,39.715 169.792,39.844 170.000 M361.724 171.954 C 361.726 173.200,361.637 175.063,361.525 176.094 C 361.413 177.125,361.234 179.336,361.127 181.006 C 358.825 217.064,347.461 239.582,327.963 246.718 C 325.065 247.779,324.284 248.053,323.910 248.143 C 323.830 248.162,323.023 248.383,322.116 248.633 C 321.210 248.884,320.117 249.142,319.688 249.208 C 319.258 249.273,317.641 249.538,316.094 249.797 C 313.333 250.258,311.392 250.513,308.281 250.822 C 306.745 250.975,306.251 251.017,302.188 251.334 C 300.248 251.485,299.357 251.578,295.577 252.021 C 293.281 252.291,288.384 253.131,287.813 253.354 C 287.555 253.454,287.168 253.508,286.953 253.473 C 286.738 253.438,286.563 253.572,286.563 253.770 C 286.563 253.968,286.365 254.054,286.124 253.961 C 285.883 253.869,285.598 253.881,285.490 253.989 C 285.383 254.097,285.123 254.216,284.913 254.254 C 279.486 255.244,280.800 255.268,340.855 255.290 L 399.679 255.313 399.766 265.391 C 399.814 270.934,399.886 254.727,399.926 229.375 C 399.966 204.023,399.906 180.223,399.793 176.484 L 399.588 169.688 380.653 169.688 L 361.719 169.689 361.724 171.954 " stroke="none" fill="#fb9b3c" fill-rule="evenodd"/><path id="path2" d="M43.051 418.013 C 42.581 418.263,41.808 418.977,41.333 419.600 L 40.469 420.732 40.387 456.698 L 40.305 492.664 41.012 494.048 C 41.401 494.809,42.116 495.722,42.601 496.075 L 43.483 496.719 199.724 496.798 C 338.186 496.868,356.068 496.823,356.873 496.408 C 357.372 496.149,358.174 495.422,358.656 494.790 L 359.531 493.642 359.531 457.212 L 359.531 420.781 358.631 419.605 C 356.922 417.371,358.257 417.478,333.366 417.572 C 308.066 417.667,309.971 417.467,308.594 420.176 C 307.992 421.360,307.965 422.117,307.881 440.391 L 307.794 459.375 289.060 459.375 L 270.326 459.375 270.241 439.914 L 270.156 420.453 269.428 419.523 C 267.785 417.426,269.037 417.528,244.756 417.513 C 220.973 417.499,221.546 417.460,220.063 419.198 C 218.773 420.710,218.750 421.077,218.750 440.489 L 218.750 459.375 200.000 459.375 L 181.250 459.375 181.250 440.507 C 181.250 418.749,181.296 419.072,178.027 417.833 C 176.227 417.152,133.830 417.407,132.284 418.108 C 129.592 419.331,129.688 418.513,129.688 440.300 L 129.688 459.375 110.938 459.375 L 92.188 459.375 92.188 440.973 C 92.188 420.244,92.175 420.112,90.046 418.489 L 88.955 417.656 66.431 417.608 C 47.275 417.567,43.778 417.628,43.051 418.013 " stroke="none" fill="#fccf5b" fill-rule="evenodd"/><path id="path3" d="M2.859 0.840 C 2.283 1.125,1.439 1.922,0.984 2.610 L 0.156 3.861 0.146 91.383 C 0.140 139.521,0.179 176.834,0.232 174.302 L 0.329 169.697 19.461 169.614 C 29.984 169.569,38.760 169.602,38.962 169.688 C 39.165 169.773,111.798 169.773,200.368 169.688 C 288.939 169.602,370.016 169.569,380.539 169.614 L 399.671 169.697 399.768 174.302 C 399.821 176.834,399.860 139.521,399.854 91.383 L 399.844 3.861 399.012 2.604 C 397.377 0.133,399.990 0.294,363.069 0.386 L 330.156 0.469 329.104 1.219 C 326.750 2.898,326.890 0.648,326.882 37.109 L 326.875 69.688 308.906 69.688 L 290.938 69.688 290.938 44.441 L 290.937 19.195 290.234 17.802 C 289.772 16.886,289.103 16.169,288.281 15.707 L 287.031 15.006 254.163 15.081 C 217.137 15.166,220.575 14.931,218.828 17.497 L 218.125 18.530 218.125 44.109 L 218.125 69.688 200.000 69.688 L 181.875 69.688 181.875 44.109 L 181.875 18.530 181.172 17.497 C 179.425 14.931,182.863 15.166,145.837 15.081 L 112.969 15.006 111.719 15.707 C 110.897 16.169,110.228 16.886,109.766 17.802 L 109.062 19.195 109.063 44.441 L 109.063 69.688 91.094 69.688 L 73.125 69.688 73.118 37.109 C 73.110 0.648,73.250 2.898,70.896 1.219 L 69.844 0.469 36.875 0.395 C 6.708 0.328,3.817 0.366,2.859 0.840 M361.648 171.250 C 361.648 172.023,361.709 172.340,361.783 171.953 C 361.858 171.566,361.858 170.934,361.783 170.547 C 361.709 170.160,361.648 170.477,361.648 171.250 M357.144 171.568 L 356.406 172.198 357.266 171.830 C 357.738 171.628,358.125 171.344,358.125 171.200 C 358.125 170.810,357.958 170.873,357.144 171.568 M43.125 171.759 C 43.125 171.921,43.547 172.302,44.063 172.607 C 44.580 172.912,45.000 173.012,45.000 172.830 C 45.000 172.649,44.835 172.500,44.634 172.500 C 44.433 172.500,44.011 172.267,43.696 171.983 C 43.382 171.698,43.125 171.598,43.125 171.759 " stroke="none" fill="#fc862e" fill-rule="evenodd"/><path id="path4" d="M40.519 334.297 C 40.406 338.035,40.317 349.602,40.320 360.000 C 40.327 381.137,40.273 380.601,42.542 382.218 L 43.594 382.969 86.025 383.050 C 123.482 383.122,128.563 383.076,129.374 382.657 C 130.775 381.932,131.789 380.671,132.162 379.189 C 132.542 377.680,132.636 345.641,132.293 334.297 L 132.088 327.500 86.406 327.500 L 40.725 327.500 40.519 334.297 M172.522 328.457 C 173.111 328.983,173.594 329.525,173.594 329.661 C 173.594 329.965,174.624 331.148,183.281 340.783 C 185.000 342.696,187.250 345.224,188.281 346.401 C 189.313 347.577,190.302 348.692,190.481 348.879 C 190.660 349.066,191.920 350.484,193.281 352.031 C 196.329 355.495,196.773 355.907,198.037 356.446 C 200.772 357.613,202.678 356.646,206.742 352.031 C 208.407 350.141,210.278 348.028,210.900 347.335 C 211.522 346.643,212.664 345.373,213.438 344.512 C 214.211 343.651,215.941 341.731,217.282 340.245 C 218.623 338.758,220.029 337.148,220.407 336.665 C 221.032 335.867,223.081 333.572,224.453 332.135 C 224.754 331.820,224.941 331.563,224.868 331.563 C 224.751 331.563,225.925 330.264,227.905 328.203 L 228.581 327.500 200.015 327.500 L 171.450 327.500 172.522 328.457 M267.707 334.297 C 267.364 345.641,267.458 377.680,267.838 379.189 C 268.211 380.671,269.225 381.932,270.626 382.657 C 271.437 383.076,276.518 383.122,313.975 383.050 L 356.406 382.969 357.458 382.218 C 359.727 380.601,359.673 381.137,359.680 360.000 C 359.683 349.602,359.594 338.035,359.481 334.297 L 359.275 327.500 313.594 327.500 L 267.912 327.500 267.707 334.297 " stroke="none" fill="#fcbc4c" fill-rule="evenodd"/></g></svg>
customer_portal/src/assets/scss/_functions.scss new
+35
@@ -0,0 +1,35 @@
1 +/*
2 +A simple little SCSS mixin for creating scrim gradients
3 +Inspired by Andreas Larson - https://github.com/larsenwork
4 +https://css-tricks.com/easing-linear-gradients/
5 +*/
6 +
7 +@function scrimGradient($startColor: $color-black, $direction: "to bottom") {
8 + $scrimCoordinates: (
9 + 0: 1,
10 + 19: 0.738,
11 + 34: 0.541,
12 + 47: 0.382,
13 + 56.5: 0.278,
14 + 65: 0.194,
15 + 73: 0.126,
16 + 80.2: 0.075,
17 + 86.1: 0.042,
18 + 91: 0.021,
19 + 95.2: 0.008,
20 + 98.2: 0.002,
21 + 100: 0
22 + );
23 +
24 + $hue: hue($startColor);
25 + $saturation: saturation($startColor);
26 + $lightness: lightness($startColor);
27 + $stops: ();
28 +
29 + @each $colorStop, $alphaValue in $scrimCoordinates {
30 + $stop: hsla($hue, $saturation, $lightness, $alphaValue) percentage($colorStop/100);
31 + $stops: append($stops, $stop, comma);
32 + }
33 +
34 + @return linear-gradient(unquote($direction), $stops);
35 +}
customer_portal/src/assets/scss/common-animations.scss new
+45
@@ -0,0 +1,45 @@
1 +.item-appear {
2 + &.item-appear-bottom {
3 + animation: item-fade-bottom 0.3s forwards;
4 + opacity: 0;
5 + }
6 + &.item-appear-up {
7 + animation: item-fade-up 0.3s forwards;
8 + opacity: 0;
9 + }
10 +
11 + &.item-appear-005 {
12 + @for $i from 0 through 40 {
13 + &:nth-child(#{$i}) {
14 + animation-delay: $i * 0.05s;
15 + }
16 + }
17 + }
18 + &.item-appear-010 {
19 + @for $i from 0 through 40 {
20 + &:nth-child(#{$i}) {
21 + animation-delay: $i * 0.1s;
22 + }
23 + }
24 + }
25 +}
26 +
27 +@keyframes item-fade-bottom {
28 + from {
29 + opacity: 0;
30 + transform: translateY(10px);
31 + }
32 + to {
33 + opacity: 1;
34 + }
35 +}
36 +
37 +@keyframes item-fade-up {
38 + from {
39 + opacity: 0;
40 + transform: translateY(-10px);
41 + }
42 + to {
43 + opacity: 1;
44 + }
45 +}
customer_portal/src/assets/scss/common.scss new
+164
@@ -0,0 +1,164 @@
1 +.custom-label {
2 + color: var(--label-color);
3 + font-size: 10px;
4 + padding: 2px 6px;
5 + border-radius: var(--border-radius-small);
6 + position: relative;
7 + display: inline-block;
8 + font-weight: bold;
9 +
10 + &::before {
11 + content: "";
12 + width: 100%;
13 + height: 100%;
14 + background-color: var(--label-color);
15 + border-radius: var(--border-radius-small);
16 + position: absolute;
17 + top: 0;
18 + left: 0;
19 + z-index: 0;
20 + opacity: 0.15;
21 + }
22 +}
23 +
24 +.page {
25 + .page-header {
26 + margin-bottom: 20px;
27 + display: flex;
28 + align-items: baseline;
29 + justify-content: space-between;
30 + gap: 20px;
31 +
32 + .title {
33 + font-family: var(--font-family-display);
34 + font-weight: bold;
35 + font-size: 30px;
36 + }
37 +
38 + .links {
39 + display: flex;
40 + gap: 20px;
41 +
42 + a {
43 + text-decoration: underline;
44 + text-decoration-color: var(--primary-color);
45 + position: relative;
46 + top: -2px;
47 +
48 + .n-icon,
49 + .n-icon-wrapper {
50 + position: relative;
51 + top: 3px;
52 + }
53 + }
54 + }
55 +
56 + @media (max-width: 700px) {
57 + flex-direction: column;
58 + gap: 6px;
59 +
60 + .title {
61 + font-size: 20px;
62 + }
63 + }
64 + }
65 + .components-list {
66 + display: grid;
67 + gap: 1.25em;
68 + align-items: start;
69 + grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
70 +
71 + @media (max-width: 400px) {
72 + grid-template-columns: repeat(auto-fit, minmax(100%, 1fr));
73 + }
74 +
75 + & > div {
76 + overflow: hidden;
77 + }
78 + }
79 +}
80 +
81 +/*
82 + DOES NOT WORK ON FIREFOX!
83 + :has() CSS relational pseudo-class not yet supported by Firefox
84 + (https://caniuse.com/css-has)
85 + you can find a worker around in pages that use this class
86 +*/
87 +#app {
88 + .layout {
89 + .main {
90 + .view:has(.page-without-footer) {
91 + & + footer {
92 + display: none;
93 + }
94 + }
95 + }
96 + }
97 +}
98 +
99 +.scrollbar-styled {
100 + /* Works on Firefox */
101 +
102 + scrollbar-width: thin;
103 + scrollbar-color: var(--hover-color) var(--bg-secondary-color);
104 +
105 + /* Works on Chrome, Edge, and Safari */
106 + &::-webkit-scrollbar,
107 + ::-webkit-scrollbar {
108 + width: 12px;
109 + }
110 +
111 + &::-webkit-scrollbar-track,
112 + ::-webkit-scrollbar-track {
113 + background: var(--bg-secondary-color);
114 + }
115 +
116 + &::-webkit-scrollbar-thumb,
117 + ::-webkit-scrollbar-thumb {
118 + background-color: var(--hover-color);
119 + border-radius: 20px;
120 + border: 2px solid var(--bg-secondary-color);
121 + }
122 +}
123 +
124 +.code-bg-transparent {
125 + code {
126 + background-color: transparent;
127 + }
128 +}
129 +
130 +.over-layer {
131 + background-color: rgba(var(--bg-body-color-rgb) / 0.8);
132 + position: absolute;
133 + top: 0;
134 + left: 0;
135 + right: 0;
136 + bottom: 0;
137 + display: flex;
138 + align-items: center;
139 + justify-content: center;
140 + padding: 40px;
141 +
142 + .n-alert {
143 + width: 90vw;
144 + max-width: 500px;
145 + background-color: var(--bg-default-color);
146 + }
147 +}
148 +
149 +.grid-auto-fit-200 {
150 + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
151 + grid-auto-flow: row dense;
152 +}
153 +.grid-auto-fit-250 {
154 + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
155 + grid-auto-flow: row dense;
156 +}
157 +.grid-auto-fill-200 {
158 + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
159 + grid-auto-flow: row dense;
160 +}
161 +.grid-auto-fill-250 {
162 + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
163 + grid-auto-flow: row dense;
164 +}
customer_portal/src/assets/scss/fonts.scss new
+3
@@ -0,0 +1,3 @@
1 +@import "@fontsource/lexend/latin.css";
2 +@import "@fontsource/public-sans/latin.css";
3 +@import "@fontsource/jetbrains-mono/latin.css";
customer_portal/src/assets/scss/index.scss new
+6
@@ -0,0 +1,6 @@
1 +@use "sass:meta";
2 +@include meta.load-css("fonts");
3 +@include meta.load-css("common");
4 +@include meta.load-css("common-animations");
5 +@include meta.load-css("router-animations");
6 +@include meta.load-css("overrides/naive-override");
customer_portal/src/assets/scss/overrides/apexchart-override.scss new
+37
@@ -0,0 +1,37 @@
1 +.apexcharts-canvas {
2 + .apexcharts-toolbar {
3 + z-index: 2;
4 +
5 + .apexcharts-menu-item {
6 + color: #000;
7 + }
8 + }
9 +
10 + foreignObject {
11 + display: none;
12 + }
13 +}
14 +
15 +.apexcharts-tooltip {
16 + backdrop-filter: blur(3px);
17 + box-shadow:
18 + 0px 5px 10px -5px rgba(0, 0, 0, 0.2),
19 + 0px 5px 20px 0px rgba(0, 0, 0, 0.2) !important;
20 +
21 + &.apexcharts-theme-dark {
22 + background: rgba(0, 0, 0, 0.5) !important;
23 + }
24 +
25 + &.apexcharts-theme-light {
26 + background: rgba(255, 255, 255, 0.5) !important;
27 + }
28 +}
29 +
30 +.direction-rtl {
31 + .apexcharts-legend {
32 + direction: ltr;
33 + }
34 + .apexcharts-title-text {
35 + direction: ltr;
36 + }
37 +}
customer_portal/src/assets/scss/overrides/naive-override.scss new
+445
@@ -0,0 +1,445 @@
1 +.n-modal-mask {
2 + backdrop-filter: blur(3px);
3 +}
4 +.n-modal,
5 +.n-card.n-modal[role] {
6 + // background-color: rgba(var(--bg-default-color-rgb) / 0.7);
7 + backdrop-filter: blur(20px);
8 + max-width: 90%;
9 + margin: 10vh auto;
10 +}
11 +.n-card {
12 + .n-card-header {
13 + flex-wrap: wrap;
14 + gap: 12px;
15 + justify-content: flex-end;
16 +
17 + .n-card-header__main {
18 + min-width: auto !important;
19 + }
20 + }
21 +}
22 +
23 +.n-drawer__resize-trigger {
24 + &::before {
25 + content: "::";
26 + background-color: var(--bg-default-color);
27 + width: 16px;
28 + height: 26px;
29 + display: flex;
30 + position: absolute;
31 + top: 50%;
32 + right: 0px;
33 + text-align: center;
34 + justify-content: center;
35 + align-items: center;
36 + border-radius: 12px 0 0 12px;
37 + letter-spacing: 1px;
38 + padding-bottom: 2px;
39 + padding-left: 4px;
40 + line-height: 0;
41 + transition: background-color 0.3s var(--n-bezier);
42 + }
43 +
44 + &.n-drawer__resize-trigger--hover {
45 + &::before {
46 + background-color: var(--primary-color);
47 + color: var(--fg-tertiary-color);
48 + }
49 + }
50 +}
51 +
52 +.n-image-preview-container {
53 + .n-image-preview-overlay {
54 + backdrop-filter: blur(3px);
55 + }
56 + .n-image-preview-toolbar {
57 + box-sizing: content-box;
58 +
59 + * {
60 + box-sizing: content-box;
61 + }
62 +
63 + & > i {
64 + font-size: clamp(18px, 5.5vw, 28px);
65 + }
66 + }
67 +}
68 +
69 +.n-slider {
70 + box-sizing: content-box;
71 +}
72 +
73 +.n-calendar * {
74 + box-sizing: content-box;
75 +}
76 +
77 +.n-badge {
78 + direction: ltr;
79 +}
80 +
81 +.n-dropdown-menu {
82 + &.n-dropdown-menu--scrollable {
83 + max-height: 60vh;
84 + }
85 +}
86 +
87 +.n-card {
88 + & > .n-card__content {
89 + max-height: 100%;
90 + }
91 +}
92 +
93 +.n-avatar .n-avatar__text {
94 + transform: translateX(-50%) translateY(-50%) scale(1);
95 +}
96 +
97 +// popover max-width
98 +.v-binder-follower-content {
99 + max-width: calc(100vw - (var(--view-padding) * 2));
100 +}
101 +
102 +.n-data-table {
103 + .n-data-table-resize-button {
104 + --n-merged-border-color: rgba(var(--fg-secondary-color-rgb) / 0.35);
105 + }
106 +}
107 +
108 +.n-spin-container {
109 + .n-spin-body {
110 + inset: 0 !important;
111 + transform: none !important;
112 + display: block;
113 +
114 + .n-base-loading {
115 + position: sticky;
116 + top: 50%;
117 + margin-top: min(50%, 100px);
118 + width: 100%;
119 + display: flex;
120 + justify-content: center;
121 + transform: translateY(-70px);
122 +
123 + .n-base-loading__transition-wrapper {
124 + width: 1em;
125 + height: 1em;
126 + }
127 + }
128 +
129 + .n-spin-description {
130 + position: sticky;
131 + top: calc(50% + 50px);
132 + left: 50%;
133 + transform: translateX(-50%) translateY(-40px);
134 + margin-top: 0;
135 + }
136 + }
137 +}
138 +
139 +.n-form-item {
140 + .n-form-item-feedback-wrapper:not(:empty) {
141 + transform: translateY(-7px);
142 + padding-left: 0px !important;
143 + padding-right: 0px !important;
144 + padding-top: 0 !important;
145 + z-index: 1;
146 +
147 + .n-form-item-feedback {
148 + font-size: 13px;
149 + line-height: 1.2;
150 + padding: 3px 12px;
151 + border-bottom-left-radius: var(--border-radius);
152 + border-bottom-right-radius: var(--border-radius);
153 + background: color-mix(in srgb, currentColor 5%, var(--bg-default-color) 95%);
154 + border: 1px solid currentColor;
155 + border-top: 0;
156 + }
157 + }
158 +
159 + &.n-form-item--small-size {
160 + .n-form-item-feedback-wrapper:not(:empty) {
161 + transform: translateY(-6px);
162 +
163 + .n-form-item-feedback {
164 + font-size: 11px;
165 + padding: 3px 10px;
166 + }
167 + }
168 + }
169 +
170 + &.n-form-item--small-large {
171 + .n-form-item-feedback-wrapper:not(:empty) {
172 + .n-form-item-feedback {
173 + padding: 3px 14px;
174 + }
175 + }
176 + }
177 +
178 + .n-form-item-blank--error {
179 + .n-upload-dragger {
180 + border-color: var(--n-feedback-text-color-error);
181 + }
182 + .n-upload-file-list:not(:empty) {
183 + margin-bottom: 24px;
184 + }
185 + }
186 +}
187 +
188 +.n-menu {
189 + .n-menu-item {
190 + .n-menu-item-content {
191 + gap: 16px;
192 +
193 + .n-menu-item-content__icon {
194 + margin-right: 0 !important;
195 + margin-left: 0 !important;
196 + }
197 + }
198 + }
199 + .n-menu-item-group {
200 + .n-menu-item-group-title {
201 + padding-left: 22px !important;
202 + padding-right: 22px !important;
203 + }
204 + }
205 +
206 + &.n-menu--horizontal {
207 + .n-menu-item {
208 + .n-menu-item-content {
209 + gap: 8px;
210 + }
211 + }
212 + }
213 +}
214 +
215 +.n-space.n-space--rtl {
216 + .n-layout {
217 + aside.n-layout-sider.n-layout-sider--left-placement.n-layout-sider--bordered {
218 + .n-layout-toggle-button {
219 + right: unset;
220 + left: 0;
221 + transform: translateX(-50%) translateY(-50%) rotate(180deg);
222 + }
223 + .n-layout-sider__border {
224 + right: unset;
225 + left: 0;
226 + }
227 + }
228 + }
229 +}
230 +
231 +.direction-rtl {
232 + .v-binder-follower-container {
233 + direction: ltr;
234 +
235 + .v-binder-follower-content {
236 + & > * {
237 + direction: rtl;
238 + }
239 + }
240 + }
241 +
242 + .n-menu {
243 + direction: rtl;
244 +
245 + &.n-menu--collapsed {
246 + .n-menu-item {
247 + .n-menu-item-content {
248 + padding-right: 0px;
249 + direction: ltr;
250 + }
251 + }
252 + }
253 + }
254 +
255 + .n-base-select-menu {
256 + .n-base-select-option {
257 + .n-base-select-option__content {
258 + .n-icon {
259 + margin-right: unset !important;
260 + margin-left: 8px;
261 + }
262 + }
263 + }
264 + }
265 +
266 + .n-base-selection {
267 + .n-base-selection-label {
268 + .n-base-selection-input {
269 + .n-base-selection-input__content {
270 + .n-icon {
271 + margin-right: unset !important;
272 + margin-left: 8px;
273 + }
274 + }
275 + }
276 + }
277 + }
278 +
279 + .n-color-picker-panel {
280 + .n-color-picker-action {
281 + gap: 8px;
282 +
283 + & > * {
284 + margin-left: 0;
285 + }
286 + }
287 + }
288 +
289 + .n-cascader-menu {
290 + direction: ltr !important;
291 + }
292 +
293 + .n-rate {
294 + direction: ltr;
295 + }
296 +
297 + .n-carousel {
298 + direction: ltr;
299 + }
300 +
301 + .n-tabs {
302 + .n-tabs-nav {
303 + direction: ltr;
304 + }
305 + }
306 +
307 + .n-timeline {
308 + &:not(.n-timeline--horizontal) {
309 + .n-timeline-item .n-timeline-item-timeline {
310 + direction: ltr;
311 + }
312 + }
313 +
314 + &.n-timeline--horizontal {
315 + .n-timeline-item .n-timeline-item-timeline {
316 + .n-timeline-item-timeline__line {
317 + right: var(--n-icon-size);
318 + left: 0;
319 + }
320 + }
321 + }
322 + }
323 +
324 + .n-data-table-filter-menu__action {
325 + gap: 8px;
326 +
327 + .n-button--rtl {
328 + margin: 0 !important;
329 + }
330 + }
331 +
332 + .n-data-table-check-extra {
333 + right: unset !important;
334 + left: -3px;
335 + }
336 +
337 + .n-page-header {
338 + .n-page-header__main {
339 + gap: 16px;
340 +
341 + & > * {
342 + margin-right: 0;
343 + margin-left: 0;
344 + }
345 + }
346 + }
347 +
348 + .n-date-panel-calendar {
349 + direction: ltr;
350 + }
351 +
352 + .n-date-panel {
353 + .n-date-panel-header {
354 + gap: 10px;
355 +
356 + & > *:not(:last-child) {
357 + margin-right: 0;
358 + }
359 + }
360 +
361 + .n-date-panel-actions {
362 + .n-date-panel-actions__suffix {
363 + margin-bottom: 0;
364 + gap: 8px;
365 + flex-wrap: wrap;
366 +
367 + .n-button {
368 + margin-bottom: 0;
369 +
370 + &:not(:last-child) {
371 + margin-right: 0;
372 + }
373 + }
374 + }
375 + }
376 +
377 + .n-date-panel-month-calendar {
378 + direction: ltr;
379 + }
380 + }
381 +
382 + .n-time-picker-panel {
383 + direction: ltr !important;
384 + }
385 +
386 + .n-form {
387 + &.n-form--inline {
388 + gap: 18px;
389 +
390 + .n-form-item {
391 + margin-right: 0;
392 + }
393 + }
394 +
395 + .n-form-item.n-form-item--left-labelled {
396 + .n-form-item-label {
397 + padding-right: 0;
398 + padding-left: 12px;
399 + }
400 + }
401 + }
402 +
403 + .n-input-group {
404 + direction: ltr;
405 + justify-content: flex-end;
406 + }
407 +
408 + .n-slider {
409 + direction: ltr;
410 + }
411 +
412 + .n-transfer {
413 + direction: ltr;
414 +
415 + .n-transfer-list-item {
416 + &.n-transfer-list-item--source {
417 + gap: 8px;
418 +
419 + & > * {
420 + margin-right: 0 !important;
421 + }
422 + }
423 + }
424 + }
425 +
426 + .n-popover {
427 + .n-popover__content {
428 + .n-popconfirm__panel {
429 + .n-popconfirm__body {
430 + .n-popconfirm__icon {
431 + margin-right: 0;
432 + margin-left: 8px;
433 + }
434 + }
435 + .n-popconfirm__action {
436 + gap: 8px;
437 +
438 + .n-button:not(:last-child) {
439 + margin-right: 0px;
440 + }
441 + }
442 + }
443 + }
444 + }
445 +}
customer_portal/src/assets/scss/overrides/vue-md-it-override.scss new
+51
@@ -0,0 +1,51 @@
1 +.vue-md-it-wrapper {
2 + img {
3 + max-width: 100%;
4 + width: 100%;
5 + border: 1px solid var(--border-color);
6 + border-radius: var(--border-radius);
7 + }
8 +
9 + table {
10 + --cell-padding: 8px 12px;
11 +
12 + width: 100%;
13 + max-width: 100%;
14 + border: 1px solid var(--border-color);
15 + border-radius: var(--border-radius);
16 + text-align: left;
17 + font-variant-numeric: tabular-nums;
18 + border-collapse: separate;
19 + border-spacing: 0;
20 + overflow-x: scroll;
21 + display: block;
22 + white-space: nowrap;
23 +
24 + thead {
25 + tr {
26 + th {
27 + white-space: nowrap;
28 + text-align: left;
29 + padding: var(--cell-padding);
30 + background-color: var(--bg-secondary-color);
31 + }
32 + }
33 + }
34 +
35 + tbody {
36 + tr {
37 + td {
38 + padding: var(--cell-padding);
39 + background-color: var(--bg-default-color);
40 + border-bottom: 1px solid var(--border-color);
41 + }
42 +
43 + &:last-child {
44 + td {
45 + border-bottom: 0;
46 + }
47 + }
48 + }
49 + }
50 + }
51 +}
customer_portal/src/assets/scss/overrides/vuesjv-override.scss new
+64
@@ -0,0 +1,64 @@
1 +.vuesjv-override {
2 + color: var(--fg-default-color) !important;
3 + font-family: var(--font-family-mono) !important;
4 + line-height: 1.7 !important;
5 +
6 + .font-mono {
7 + font-family: var(--font-family-mono) !important;
8 + }
9 +
10 + i {
11 + font-size: 1rem;
12 +
13 + &.i-mdi-content-copy {
14 + font-size: 0.85rem;
15 + }
16 + }
17 +
18 + .bg-gray-200 {
19 + background-color: rgba(var(--border-color-rgb) / 0.1);
20 + }
21 + .c-gray-400 {
22 + color: var(--fg-secondary-color);
23 + }
24 + .c-indigo-600 {
25 + color: var(--extra2-color);
26 + }
27 + .c-blue-600,
28 + .c-purple-600,
29 + .c-red-600,
30 + .c-pink-600,
31 + .c-blue-900,
32 + .c-blue-600,
33 + .c-amber-600 {
34 + color: var(--primary-color);
35 + font-weight: bold;
36 + }
37 + .hover\:bg-gray-100 {
38 + &:hover {
39 + background-color: transparent;
40 + }
41 + }
42 +
43 + .cursor-pointer {
44 + color: var(--fg-default-color);
45 + background-color: transparent;
46 + height: 18px;
47 + border-radius: 0;
48 +
49 + &:has(.i-mdi-content-copy) {
50 + width: 30px;
51 + }
52 +
53 + &:hover {
54 + color: var(--primary-color);
55 + background-color: transparent;
56 + }
57 + }
58 +
59 + .flex.flex-col > .flex {
60 + & > div:first-child {
61 + white-space: nowrap;
62 + }
63 + }
64 +}
customer_portal/src/assets/scss/router-animations.scss new
+73
@@ -0,0 +1,73 @@
1 +/* router-fade */
2 +.router-fade-enter-active,
3 +.router-fade-leave-active {
4 + transition: opacity var(--router-transition-duration) ease-in-out;
5 +}
6 +.router-fade-enter-from,
7 +.router-fade-leave-to {
8 + opacity: 0;
9 +}
10 +
11 +/* router-fade-up */
12 +.router-fade-up-enter-active,
13 +.router-fade-up-leave-active {
14 + transition:
15 + opacity var(--router-transition-duration) ease-in-out,
16 + transform var(--router-transition-duration) ease-in-out;
17 +}
18 +.router-fade-up-enter-from {
19 + opacity: 0;
20 + transform: translateY(10px);
21 +}
22 +.router-fade-up-leave-to {
23 + opacity: 0;
24 + transform: translateY(-10px);
25 +}
26 +
27 +/* router-fade-bottom */
28 +.router-fade-bottom-enter-active,
29 +.router-fade-bottom-leave-active {
30 + transition:
31 + opacity var(--router-transition-duration) ease-in-out,
32 + transform var(--router-transition-duration) ease-in-out;
33 +}
34 +.router-fade-bottom-enter-from {
35 + opacity: 0;
36 + transform: translateY(-10px);
37 +}
38 +.router-fade-bottom-leave-to {
39 + opacity: 0;
40 + transform: translateY(10px);
41 +}
42 +
43 +/* router-fade-left */
44 +.router-fade-left-enter-active,
45 +.router-fade-left-leave-active {
46 + transition:
47 + opacity var(--router-transition-duration) ease-in-out,
48 + transform var(--router-transition-duration) ease-in-out;
49 +}
50 +.router-fade-left-enter-from {
51 + opacity: 0;
52 + transform: translateX(10px);
53 +}
54 +.router-fade-left-leave-to {
55 + opacity: 0;
56 + transform: translateX(-10px);
57 +}
58 +
59 +/* router-fade-right */
60 +.router-fade-right-enter-active,
61 +.router-fade-right-leave-active {
62 + transition:
63 + opacity var(--router-transition-duration) ease-in-out,
64 + transform var(--router-transition-duration) ease-in-out;
65 +}
66 +.router-fade-right-enter-from {
67 + opacity: 0;
68 + transform: translateX(-10px);
69 +}
70 +.router-fade-right-leave-to {
71 + opacity: 0;
72 + transform: translateX(10px);
73 +}
customer_portal/src/components/common/Icon.vue new
+59
@@ -0,0 +1,59 @@
1 +<template>
2 + <component :is="componentName" v-bind="options">
3 + <template v-if="$slots.default">
4 + <slot />
5 + </template>
6 + <template v-else>
7 + <Icon v-if="icon" :icon :width="options.size || undefined" :height="options.size || undefined" />
8 + </template>
9 + </component>
10 +</template>
11 +
12 +<script setup lang="ts">
13 +import type { IconifyIcon } from "@iconify/vue"
14 +import { Icon, loadIcon } from "@iconify/vue"
15 +import { NIcon, NIconWrapper } from "naive-ui"
16 +import { computed, ref, watchEffect } from "vue"
17 +
18 +const props = defineProps<{
19 + name?: string
20 + size?: number
21 + bgSize?: number
22 + color?: string
23 + bgColor?: string
24 + borderRadius?: number
25 + depth?: 1 | 2 | 3 | 4 | 5
26 +}>()
27 +
28 +const useWrapper = computed(() => !!(props.bgColor || props.bgSize || props.borderRadius))
29 +const componentName = computed(() => (useWrapper.value ? NIconWrapper : NIcon))
30 +
31 +const options = computed(() => {
32 + const opt: Partial<{ size: number; color: string; borderRadius: number; iconColor: string; depth: number }> = {}
33 + if (useWrapper.value) {
34 + if (props.bgSize !== undefined) opt.size = props.bgSize
35 + if (props.bgColor !== undefined) opt.color = props.bgColor
36 + if (props.borderRadius !== undefined) opt.borderRadius = props.borderRadius
37 + if (props.color !== undefined) opt.iconColor = props.color
38 + } else {
39 + if (props.color !== undefined) opt.color = props.color
40 + if (props.depth !== undefined) opt.depth = props.depth
41 + if (props.size !== undefined) opt.size = props.size
42 + }
43 + return opt
44 +})
45 +
46 +const load = (name: string) => loadIcon(name).catch(() => console.error(`Failed to load icon ${name}`))
47 +
48 +const icon = ref<void | Required<IconifyIcon>>()
49 +
50 +function setIcon(name: string | undefined) {
51 + if (name) {
52 + load(name).then(res => (icon.value = res))
53 + }
54 +}
55 +
56 +setIcon(props.name)
57 +
58 +watchEffect(() => setIcon(props.name))
59 +</script>
customer_portal/src/components/common/cards/CardLink.vue new
+40
@@ -0,0 +1,40 @@
1 +<template>
2 + <n-card size="small" :class="{ 'group cursor-pointer': clickable }" :hoverable>
3 + <div class="flex flex-col gap-2">
4 + <div class="flex items-center justify-between gap-2">
5 + <span
6 + class="text-secondary text-sm transition-colors"
7 + :class="{ 'group-hover:text-primary': clickable }"
8 + >
9 + <slot name="title">{{ title }}</slot>
10 + </span>
11 + <Icon
12 + v-if="clickable"
13 + name="carbon:arrow-up-right"
14 + class="group-hover:text-primary transition-colors"
15 + />
16 + <slot name="header-extra" />
17 + </div>
18 + <slot />
19 + <div class="font-mono text-xl font-semibold">
20 + <slot name="value">{{ value }}</slot>
21 + </div>
22 + <div v-if="subtitle || $slots.subtitle" class="text-secondary text-xs">
23 + <slot name="subtitle">{{ subtitle }}</slot>
24 + </div>
25 + </div>
26 + </n-card>
27 +</template>
28 +
29 +<script setup lang="ts">
30 +import { NCard } from "naive-ui"
31 +import Icon from "@/components/common/Icon.vue"
32 +
33 +defineProps<{
34 + title?: string | number
35 + value?: string | number
36 + subtitle?: string | number
37 + hoverable?: boolean
38 + clickable?: boolean
39 +}>()
40 +</script>
customer_portal/src/components/common/charts/ChartBar.vue new
+145
@@ -0,0 +1,145 @@
1 +<template>
2 + <div :style="{ height, width: '100%' }">
3 + <apexchart width="100%" :height :options="chartOptions" :series="chartSeries" />
4 + </div>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import type { ApexOptions } from "apexcharts"
9 +import { computed } from "vue"
10 +import apexchart from "vue3-apexcharts"
11 +import { useThemeStore } from "@/stores/theme"
12 +import { DASHBOARD_CHART_COLORS } from "./chartColors"
13 +import "@/assets/scss/overrides/apexchart-override.scss"
14 +
15 +const props = withDefaults(
16 + defineProps<{
17 + labels?: string[]
18 + data?: number[]
19 + height?: string
20 + monochrome?: boolean
21 + }>(),
22 + {
23 + labels: () => [],
24 + data: () => [],
25 + height: "100%"
26 + }
27 +)
28 +
29 +const emit = defineEmits<{
30 + itemClick: [item: { name: string }]
31 +}>()
32 +
33 +const themeStore = useThemeStore()
34 +
35 +const chartSeries = computed(() => [
36 + {
37 + name: "value",
38 + data: props.labels.map((_, i) => Number(props.data[i] ?? 0))
39 + }
40 +])
41 +
42 +const chartOptions = computed<ApexOptions>(() => {
43 + const style = themeStore.style
44 + const fg = style["fg-default-color"]
45 + const bc = style["border-color"]
46 + const ff = style["font-family"]
47 +
48 + return {
49 + chart: {
50 + type: "bar",
51 + fontFamily: ff,
52 + toolbar: { show: false },
53 + animations: { enabled: true },
54 + events: {
55 + dataPointSelection(_event, _chartContext, config) {
56 + const idx = config?.dataPointIndex ?? 0
57 + const name = props.labels[idx]
58 + if (name) emit("itemClick", { name })
59 + }
60 + }
61 + },
62 + responsive: [
63 + {
64 + breakpoint: 1080,
65 + options: {
66 + yaxis: {
67 + labels: {
68 + style: { colors: fg, fontSize: "10px" },
69 + maxWidth: 100
70 + }
71 + }
72 + }
73 + }
74 + ],
75 + colors: props.monochrome ? [DASHBOARD_CHART_COLORS[0]] : DASHBOARD_CHART_COLORS,
76 + states: {
77 + active: {
78 + allowMultipleDataPointsSelection: false,
79 + filter: {
80 + type: "none"
81 + }
82 + }
83 + },
84 + plotOptions: {
85 + bar: {
86 + expandOnClick: false,
87 + horizontal: true,
88 + distributed: true,
89 + borderRadius: 4,
90 + barHeight: "60%",
91 + dataLabels: {
92 + position: "top"
93 + }
94 + }
95 + },
96 + dataLabels: {
97 + enabled: true,
98 + textAnchor: "start",
99 + offsetX: 5,
100 + style: {
101 + fontSize: "12px",
102 + colors: ["#fff"]
103 + }
104 + },
105 + stroke: { show: false },
106 + xaxis: {
107 + categories: [...props.labels],
108 + labels: {
109 + style: { colors: fg, fontSize: "10px" }
110 + },
111 + axisBorder: { show: false },
112 + axisTicks: { show: false }
113 + },
114 + yaxis: {
115 + labels: {
116 + style: { colors: fg, fontSize: "11px" },
117 + maxWidth: 200,
118 + trim: true
119 + }
120 + },
121 + grid: {
122 + borderColor: bc,
123 + strokeDashArray: 0,
124 + xaxis: { lines: { show: true } },
125 + yaxis: { lines: { show: false } }
126 + },
127 + legend: { show: false },
128 + tooltip: {
129 + theme: themeStore.isThemeDark ? "dark" : "light",
130 + y: {
131 + formatter: (val: number) => String(val)
132 + }
133 + },
134 + noData: {
135 + text: "No data",
136 + align: "center",
137 + style: {
138 + color: fg,
139 + fontSize: "16px",
140 + fontFamily: ff
141 + }
142 + }
143 + }
144 +})
145 +</script>
customer_portal/src/components/common/charts/ChartColumn.vue new
+168
@@ -0,0 +1,168 @@
1 +<template>
2 + <div :style="{ height, width: '100%' }">
3 + <apexchart width="100%" :height :options="chartOptions" :series="chartSeries" />
4 + </div>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import type { ApexOptions } from "apexcharts"
9 +import { computed } from "vue"
10 +import apexchart from "vue3-apexcharts"
11 +import { useSettingsStore } from "@/stores/settings"
12 +import { useThemeStore } from "@/stores/theme"
13 +import dayjs from "@/utils/dayjs"
14 +import { DASHBOARD_CHART_COLORS } from "./chartColors"
15 +import "@/assets/scss/overrides/apexchart-override.scss"
16 +
17 +const props = withDefaults(
18 + defineProps<{
19 + labels?: string[]
20 + data?: number[]
21 + height?: string
22 + monochrome?: boolean
23 + labelsDatetime?: boolean
24 + }>(),
25 + {
26 + labels: () => [],
27 + data: () => [],
28 + height: "100%"
29 + }
30 +)
31 +
32 +const emit = defineEmits<{
33 + itemClick: [item: { name: string }]
34 +}>()
35 +
36 +const themeStore = useThemeStore()
37 +const dFormats = useSettingsStore().dateFormat
38 +
39 +const chartSeries = computed(() => [
40 + {
41 + name: "value",
42 + data: props.labels.map((_, i) => Number(props.data[i] ?? 0))
43 + }
44 +])
45 +
46 +const chartOptions = computed<ApexOptions>(() => {
47 + const style = themeStore.style
48 + const fg = style["fg-default-color"]
49 + const bc = style["border-color"]
50 + const ff = style["font-family"]
51 +
52 + return {
53 + chart: {
54 + type: "bar",
55 + fontFamily: ff,
56 + toolbar: { show: false },
57 + animations: { enabled: true },
58 + events: {
59 + dataPointSelection(_event, _chartContext, config) {
60 + const idx = config?.dataPointIndex ?? 0
61 + const name = props.labels[idx]
62 + if (name) emit("itemClick", { name })
63 + }
64 + }
65 + },
66 + responsive: [
67 + {
68 + breakpoint: 1100,
69 + options: {
70 + xaxis: {
71 + tickAmount: 10,
72 + labels: {
73 + show: true
74 + }
75 + }
76 + }
77 + },
78 + {
79 + breakpoint: 750,
80 + options: {
81 + xaxis: {
82 + tickAmount: 5,
83 + labels: {
84 + show: true
85 + }
86 + }
87 + }
88 + },
89 + {
90 + breakpoint: 500,
91 + options: {
92 + xaxis: {
93 + labels: {
94 + show: false
95 + }
96 + }
97 + }
98 + }
99 + ],
100 + colors: props.monochrome ? [DASHBOARD_CHART_COLORS[0]] : DASHBOARD_CHART_COLORS,
101 + states: {
102 + active: {
103 + allowMultipleDataPointsSelection: false,
104 + filter: {
105 + type: "none"
106 + }
107 + }
108 + },
109 + plotOptions: {
110 + bar: {
111 + expandOnClick: false,
112 + horizontal: false,
113 + distributed: true,
114 + borderRadius: 4,
115 + borderRadiusApplication: "end",
116 + columnWidth: "60%"
117 + }
118 + },
119 + dataLabels: { enabled: false },
120 + stroke: { show: false },
121 + xaxis: {
122 + categories: Array.from(props.labels, label => {
123 + return props.labelsDatetime
124 + ? [dayjs(label).format(dFormats.date), dayjs(label).format(dFormats.time)]
125 + : label
126 + }),
127 + tickAmount: 20,
128 + labels: {
129 + show: true,
130 + style: { colors: fg, fontSize: "11px" },
131 + rotate: -50,
132 + rotateAlways: true,
133 + hideOverlappingLabels: true,
134 + trim: false
135 + },
136 + axisBorder: { show: false },
137 + axisTicks: { show: false }
138 + },
139 + yaxis: {
140 + labels: {
141 + style: { colors: fg, fontSize: "10px" }
142 + }
143 + },
144 + grid: {
145 + borderColor: bc,
146 + strokeDashArray: 0,
147 + xaxis: { lines: { show: false } },
148 + yaxis: { lines: { show: true } }
149 + },
150 + legend: { show: false },
151 + tooltip: {
152 + theme: themeStore.isThemeDark ? "dark" : "light",
153 + y: {
154 + formatter: (val: number) => String(val)
155 + }
156 + },
157 + noData: {
158 + text: "No data",
159 + align: "center",
160 + style: {
161 + color: fg,
162 + fontSize: "16px",
163 + fontFamily: ff
164 + }
165 + }
166 + }
167 +})
168 +</script>
customer_portal/src/components/common/charts/ChartPie.vue new
+123
@@ -0,0 +1,123 @@
1 +<template>
2 + <div :style="{ height, width: '100%' }">
3 + <apexchart width="100%" :height :options="chartOptions" :series="chartSeries" />
4 + </div>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import type { ApexOptions } from "apexcharts"
9 +import { computed } from "vue"
10 +import apexchart from "vue3-apexcharts"
11 +import { useThemeStore } from "@/stores/theme"
12 +import { DASHBOARD_CHART_COLORS } from "./chartColors"
13 +import "@/assets/scss/overrides/apexchart-override.scss"
14 +
15 +const props = withDefaults(
16 + defineProps<{
17 + labels?: string[]
18 + data?: number[]
19 + height?: string
20 + monochrome?: boolean
21 + }>(),
22 + {
23 + labels: () => [],
24 + data: () => [],
25 + height: "100%"
26 + }
27 +)
28 +
29 +const emit = defineEmits<{
30 + itemClick: [item: { name: string }]
31 +}>()
32 +
33 +const themeStore = useThemeStore()
34 +
35 +const chartSeries = computed<number[]>(() => {
36 + if (!props.labels.length) return []
37 + return props.labels.map((_, i) => Number(props.data[i] ?? 0))
38 +})
39 +
40 +const chartOptions = computed<ApexOptions>(() => {
41 + const style = themeStore.style
42 + const fg = style["fg-default-color"]
43 + const ff = style["font-family"]
44 +
45 + return {
46 + chart: {
47 + type: "donut",
48 + fontFamily: ff,
49 + toolbar: { show: false },
50 + animations: { enabled: true },
51 + events: {
52 + dataPointSelection(_event, _chartContext, config) {
53 + const idx = config?.dataPointIndex ?? 0
54 + const name = props.labels[idx]
55 + if (name) emit("itemClick", { name })
56 + }
57 + }
58 + },
59 + labels: [...props.labels],
60 + colors: props.monochrome ? [DASHBOARD_CHART_COLORS[0]] : DASHBOARD_CHART_COLORS,
61 + states: {
62 + active: {
63 + allowMultipleDataPointsSelection: false,
64 + filter: {
65 + type: "none"
66 + }
67 + }
68 + },
69 + plotOptions: {
70 + pie: {
71 + expandOnClick: false,
72 + donut: {
73 + size: "55%",
74 + labels: {
75 + show: true,
76 + name: { fontFamily: ff, fontSize: "12px", offsetY: -2 },
77 + value: { color: fg, fontFamily: ff, fontSize: "12px", offsetY: 0 },
78 + total: { color: fg, fontFamily: ff, fontSize: "12px", offsetY: 2 }
79 + }
80 + }
81 + }
82 + },
83 + dataLabels: {
84 + enabled: true
85 + },
86 + stroke: {
87 + show: false
88 + },
89 + legend: {
90 + show: true,
91 + position: "right",
92 + fontSize: "11px",
93 + fontFamily: ff,
94 + labels: { colors: fg },
95 + itemMargin: { vertical: 6, horizontal: 4 },
96 + markers: {
97 + size: 7,
98 + strokeWidth: 0,
99 + offsetX: -5
100 + },
101 + formatter: (seriesName, opts) => {
102 + const series = opts?.w.globals.series as number[]
103 + const value = series[opts?.seriesIndex ?? 0] ?? 0
104 + const total = series.reduce((sum, v) => sum + v, 0)
105 + const pct = total > 0 ? (value / total) * 100 : 0
106 + return `${seriesName} - ${value} (${pct.toFixed(1)}%)`
107 + }
108 + },
109 + tooltip: {
110 + theme: themeStore.isThemeDark ? "dark" : "light"
111 + },
112 + noData: {
113 + text: "No data",
114 + align: "center",
115 + style: {
116 + color: fg,
117 + fontSize: "16px",
118 + fontFamily: ff
119 + }
120 + }
121 + }
122 +})
123 +</script>
customer_portal/src/components/common/charts/chartColors.ts new
+17
@@ -0,0 +1,17 @@
1 +/** Palette shared by dashboard charts (pie segments, etc.). */
2 +export const DASHBOARD_CHART_COLORS = [
3 + "#8a3ffc",
4 + "#33b1ff",
5 + "#007d79",
6 + "#ff7eb6",
7 + "#fa4d56",
8 + "#fff1f1",
9 + "#6fdc8c",
10 + "#4589ff",
11 + "#d12771",
12 + "#d2a106",
13 + "#08bdba",
14 + "#bae6ff",
15 + "#ba4e00",
16 + "#d4bbff"
17 +]
customer_portal/src/components/dashboards/DashboardCategoriesSection.vue new
+186
@@ -0,0 +1,186 @@
1 +<template>
2 + <n-card size="small" class="@container">
3 + <template #header>Dashboard Categories</template>
4 + <template #header-extra>
5 + <span class="text-secondary text-sm">{{ categories.length }} available</span>
6 + </template>
7 +
8 + <n-spin :show="loadingCategories">
9 + <div
10 + v-if="categories.length"
11 + class="grid grid-cols-1 gap-3 @xl:grid-cols-2 @3xl:grid-cols-3 @6xl:grid-cols-4"
12 + >
13 + <DashboardCategoryCard
14 + v-for="cat in categories"
15 + :key="cat.id"
16 + :category="cat"
17 + :selected="selectedCategoryId === cat.id"
18 + @select="selectCategory(cat.id)"
19 + />
20 + </div>
21 + <n-empty v-else-if="!loadingCategories" description="No dashboard categories found" />
22 + </n-spin>
23 +
24 + <div v-if="selectedCategory" class="border-border mt-4 flex flex-col gap-4 border-t pt-4">
25 + <div class="flex items-center justify-between gap-2">
26 + <div class="flex grow items-center justify-between">
27 + <div class="flex items-center gap-2">
28 + <div class="flex h-full items-center justify-center" :style="{ color: selectedCategory.color }">
29 + <Icon :name="getDashboardIcon(selectedCategory.icon)" :size="19" />
30 + </div>
31 + <span>{{ selectedCategory.title }}</span>
32 + </div>
33 + <span class="text-sm font-normal opacity-60">
34 + {{ selectedCategory.templates.length }} template{{
35 + selectedCategory.templates.length !== 1 ? "s" : ""
36 + }}
37 + </span>
38 + </div>
39 +
40 + <n-select
41 + v-model:value="selectedEventSourceId"
42 + :options="eventSourceOptions"
43 + placeholder="Select Event Source"
44 + filterable
45 + :loading="loadingEventSources"
46 + :disabled="!selectedCustomerCode"
47 + clearable
48 + size="small"
49 + :consistent-menu-width="false"
50 + class="w-48!"
51 + />
52 + </div>
53 +
54 + <n-spin :show="loadingTemplates">
55 + <div
56 + v-if="selectedCategory.templates.length"
57 + class="grid grid-cols-1 gap-3 @xl:grid-cols-2 @3xl:grid-cols-3 @6xl:grid-cols-4"
58 + >
59 + <DashboardTemplateCard
60 + v-for="tpl in selectedCategory.templates"
61 + :key="tpl.id"
62 + :template="tpl"
63 + :selected-customer-code
64 + :selected-event-source-id
65 + :selected-category-id
66 + :enabled-dashboards
67 + disabled-tooltip-text="Select an event source first"
68 + @refresh-enabled-dashboards="emit('refreshEnabledDashboards')"
69 + />
70 + </div>
71 + <n-empty v-else-if="!loadingTemplates" description="No templates in this category" />
72 + </n-spin>
73 + </div>
74 + </n-card>
75 +</template>
76 +
77 +<script setup lang="ts">
78 +import type { DashboardCategory, DashboardCategoryWithTemplates, EnabledDashboard } from "@/types/dashboards.d"
79 +import type { EventSource } from "@/types/eventSources.d"
80 +import { NCard, NEmpty, NSelect, NSpin, useMessage } from "naive-ui"
81 +import { computed, onBeforeMount, ref, watch } from "vue"
82 +import Api from "@/api"
83 +import Icon from "@/components/common/Icon.vue"
84 +import DashboardCategoryCard from "./DashboardCategoryCard.vue"
85 +import DashboardTemplateCard from "./DashboardTemplateCard.vue"
86 +import { getDashboardIcon } from "./utils"
87 +
88 +const props = defineProps<{
89 + selectedCustomerCode: string | null
90 + eventSourcesList: EventSource[]
91 + loadingEventSources: boolean
92 + enabledDashboards: EnabledDashboard[]
93 +}>()
94 +
95 +const emit = defineEmits<{
96 + refreshEnabledDashboards: []
97 +}>()
98 +
99 +const message = useMessage()
100 +
101 +const loadingCategories = ref(false)
102 +const categories = ref<DashboardCategory[]>([])
103 +const selectedCategoryId = ref<string | null>(null)
104 +const selectedCategory = ref<DashboardCategoryWithTemplates | null>(null)
105 +const loadingTemplates = ref(false)
106 +const selectedEventSourceId = ref<number | null>(null)
107 +
108 +const eventSourceOptions = computed(() =>
109 + props.eventSourcesList
110 + .filter(source => source.enabled)
111 + .map(source => ({
112 + label: `${source.name} (${source.event_type})`,
113 + value: source.id
114 + }))
115 +)
116 +
117 +function getCategories() {
118 + loadingCategories.value = true
119 + Api.siem
120 + .getDashboardCategories()
121 + .then(res => {
122 + if (res.data.success) {
123 + categories.value = res.data?.categories || []
124 + } else {
125 + message.warning(res.data?.message || "An error occurred. Please try again later.")
126 + }
127 + })
128 + .catch(err => {
129 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
130 + })
131 + .finally(() => {
132 + loadingCategories.value = false
133 + })
134 +}
135 +
136 +function selectCategory(categoryId: string) {
137 + if (selectedCategoryId.value === categoryId) {
138 + selectedCategoryId.value = null
139 + selectedCategory.value = null
140 + return
141 + }
142 +
143 + selectedCategoryId.value = categoryId
144 + loadingTemplates.value = true
145 +
146 + Api.siem
147 + .getDashboardCategory(categoryId)
148 + .then(res => {
149 + if (res.data.success) {
150 + selectedCategory.value = res.data.category
151 + } else {
152 + message.warning(res.data?.message || "An error occurred. Please try again later.")
153 + }
154 + })
155 + .catch(err => {
156 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
157 + })
158 + .finally(() => {
159 + loadingTemplates.value = false
160 + })
161 +}
162 +
163 +watch(
164 + () => props.selectedCustomerCode,
165 + () => {
166 + selectedEventSourceId.value = null
167 + }
168 +)
169 +
170 +watch(
171 + () => props.eventSourcesList,
172 + sources => {
173 + if (!selectedEventSourceId.value) return
174 +
175 + const hasSelectedSource = sources.some(source => source.enabled && source.id === selectedEventSourceId.value)
176 +
177 + if (!hasSelectedSource) {
178 + selectedEventSourceId.value = null
179 + }
180 + }
181 +)
182 +
183 +onBeforeMount(() => {
184 + getCategories()
185 +})
186 +</script>
customer_portal/src/components/dashboards/DashboardCategoryCard.vue new
+52
@@ -0,0 +1,52 @@
1 +<template>
2 + <CardEntity hoverable clickable size="small" :class="{ highlighted: selected }" @click="$emit('select')">
3 + <template #headerMain>
4 + <div class="flex items-center gap-2">
5 + <div class="flex h-full items-center justify-center" :style="{ color: category.color }">
6 + <Icon :name="getDashboardIcon(category.icon)" :size="16" />
7 + </div>
8 + {{ category.title }}
9 + </div>
10 + </template>
11 + <template #default>
12 + <div class="flex flex-col gap-2">
13 + <p class="text-xs">
14 + {{ category.description }}
15 + </p>
16 + <div v-if="category.tags.length" class="text-tertiary flex flex-wrap gap-2 text-xs">
17 + <span v-for="tag in category.tags" :key="tag">#{{ tag }}</span>
18 + </div>
19 + </div>
20 + </template>
21 +
22 + <template #footerMain>
23 + <div class="flex flex-wrap gap-2">
24 + <Badge type="splitted">
25 + <template #label>Vendor</template>
26 + <template #value>{{ category.vendor }}</template>
27 + </Badge>
28 + <Badge type="splitted">
29 + <template #label>Type</template>
30 + <template #value>{{ category.event_type }}</template>
31 + </Badge>
32 + </div>
33 + </template>
34 + </CardEntity>
35 +</template>
36 +
37 +<script setup lang="ts">
38 +import type { DashboardCategory } from "@/types/dashboards.d"
39 +import Badge from "@/components/common/Badge.vue"
40 +import CardEntity from "@/components/common/cards/CardEntity.vue"
41 +import Icon from "@/components/common/Icon.vue"
42 +import { getDashboardIcon } from "./utils"
43 +
44 +defineProps<{
45 + category: DashboardCategory
46 + selected?: boolean
47 +}>()
48 +
49 +defineEmits<{
50 + select: []
51 +}>()
52 +</script>
customer_portal/src/components/dashboards/DashboardTemplateCard.vue new
+144
@@ -0,0 +1,144 @@
1 +<template>
2 + <CardEntity hoverable size="small">
3 + <template #headerMain>
4 + {{ template.title }}
5 + </template>
6 + <template #default>
7 + <p class="text-xs">
8 + {{ template.description }}
9 + </p>
10 + </template>
11 +
12 + <template #footerMain>
13 + <Badge type="splitted">
14 + <template #label>Panels</template>
15 + <template #value>{{ template.panels.length }}</template>
16 + </Badge>
17 + </template>
18 +
19 + <template #footerExtra>
20 + <n-tooltip v-if="!isEnabled" :disabled="!disabledTooltipText" class="px-2! py-1!">
21 + <template #trigger>
22 + <n-button size="small" type="primary" :disabled="!canEnable" @click="onEnable">
23 + <template #icon>
24 + <Icon :name="disabledTooltipText ? LockedIcon : EnableIcon" />
25 + </template>
26 + Enable
27 + </n-button>
28 + </template>
29 + <div class="text-sm">
30 + {{ disabledTooltipText }}
31 + </div>
32 + </n-tooltip>
33 + <n-button v-else size="small" type="error" quaternary @click="onDisable">
34 + <template #icon>
35 + <Icon :name="DisableIcon" />
36 + </template>
37 + Disable
38 + </n-button>
39 + </template>
40 + </CardEntity>
41 +</template>
42 +
43 +<script setup lang="ts">
44 +import type { DashboardTemplate, EnabledDashboard } from "@/types/dashboards.d"
45 +import { NButton, NTooltip, useDialog, useMessage } from "naive-ui"
46 +import { computed } from "vue"
47 +import Api from "@/api"
48 +import Badge from "@/components/common/Badge.vue"
49 +import CardEntity from "@/components/common/cards/CardEntity.vue"
50 +import Icon from "@/components/common/Icon.vue"
51 +
52 +const props = defineProps<{
53 + template: DashboardTemplate
54 + selectedCustomerCode: string | null
55 + selectedEventSourceId: number | null
56 + selectedCategoryId: string | null
57 + enabledDashboards: EnabledDashboard[]
58 + disabledTooltipText?: string
59 +}>()
60 +
61 +const emit = defineEmits<{
62 + refreshEnabledDashboards: []
63 +}>()
64 +
65 +const message = useMessage()
66 +const dialog = useDialog()
67 +
68 +const EnableIcon = "carbon:add-alt"
69 +const DisableIcon = "carbon:subtract-alt"
70 +const LockedIcon = "carbon:locked"
71 +
72 +const canEnable = computed(() => !!props.selectedCustomerCode && !!props.selectedEventSourceId)
73 +
74 +const isEnabled = computed(() => {
75 + if (!props.selectedCategoryId || !props.selectedEventSourceId) return false
76 +
77 + return props.enabledDashboards.some(
78 + d =>
79 + d.library_card === props.selectedCategoryId &&
80 + d.template_id === props.template.id &&
81 + d.event_source_id === props.selectedEventSourceId
82 + )
83 +})
84 +
85 +function onEnable() {
86 + if (!props.selectedCustomerCode || !props.selectedEventSourceId || !props.selectedCategoryId) {
87 + message.warning("Please select a customer and event source first")
88 + return
89 + }
90 +
91 + Api.siem
92 + .enableDashboard({
93 + customer_code: props.selectedCustomerCode,
94 + event_source_id: props.selectedEventSourceId,
95 + library_card: props.selectedCategoryId,
96 + template_id: props.template.id,
97 + display_name: props.template.title
98 + })
99 + .then(res => {
100 + if (res.data.success) {
101 + message.success(res.data?.message || "Dashboard enabled successfully")
102 + emit("refreshEnabledDashboards")
103 + } else {
104 + message.warning(res.data?.message || "An error occurred. Please try again later.")
105 + }
106 + })
107 + .catch(err => {
108 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
109 + })
110 +}
111 +
112 +function onDisable() {
113 + const match = props.enabledDashboards.find(
114 + d =>
115 + d.library_card === props.selectedCategoryId &&
116 + d.template_id === props.template.id &&
117 + d.event_source_id === props.selectedEventSourceId
118 + )
119 +
120 + if (!match) return
121 +
122 + dialog.warning({
123 + title: "Disable Dashboard",
124 + content: `Are you sure you want to disable "${props.template.title}"?`,
125 + positiveText: "Disable",
126 + negativeText: "Cancel",
127 + onPositiveClick: () => {
128 + Api.siem
129 + .disableDashboard(match.id)
130 + .then(res => {
131 + if (res.data.success) {
132 + message.success(res.data?.message || "Dashboard disabled successfully")
133 + emit("refreshEnabledDashboards")
134 + } else {
135 + message.warning(res.data?.message || "An error occurred. Please try again later.")
136 + }
137 + })
138 + .catch(err => {
139 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
140 + })
141 + }
142 + })
143 +}
144 +</script>
customer_portal/src/components/dashboards/DashboardViewer.vue new
+202
@@ -0,0 +1,202 @@
1 +<template>
2 + <div class="@container flex flex-col gap-8">
3 + <!-- Header Bar -->
4 + <div class="flex flex-wrap items-end justify-between gap-6">
5 + <div class="flex gap-3">
6 + <!-- TODO-FE: use router by name -->
7 + <n-button quaternary size="small" @click="router.push('/dashboards')">
8 + <template #icon>
9 + <Icon :name="ArrowBackIcon" :size="22" />
10 + </template>
11 + </n-button>
12 + <div class="flex flex-col">
13 + <span class="text-lg font-semibold">{{ dashboardTitle }}</span>
14 + <span class="text-xs opacity-60">{{ dashboardDescription }}</span>
15 + </div>
16 + </div>
17 + <div class="flex grow items-center justify-end gap-2">
18 + <n-radio-group v-model:value="selectedTimerange" size="small">
19 + <n-radio-button v-for="preset in timePresets" :key="preset" :value="preset" :label="preset" />
20 + </n-radio-group>
21 +
22 + <n-button size="small" :loading @click="fetchPanelData">
23 + <template #icon>
24 + <Icon :name="RefreshIcon" :size="16" />
25 + </template>
26 + </n-button>
27 + </div>
28 + </div>
29 +
30 + <!-- Panels Grid -->
31 + <n-spin :show="loading" content-class="grid grid-cols-12 gap-4">
32 + <CardLink
33 + v-for="item in panels"
34 + :key="item.panel.id"
35 + :title="item.panel.title"
36 + class="h-full"
37 + :class="[panelColSpanClass(item.panel.w)]"
38 + :clickable="['stat'].includes(item.panel.type)"
39 + @click="['stat'].includes(item.panel.type) ? openEventSearch(item.panel.lucene || '*') : undefined"
40 + >
41 + <template v-if="['pie', 'bar_h'].includes(item.panel.type)" #header-extra>
42 + <n-tooltip class="py1! px2!">
43 + <template #trigger>
44 + <Icon :name="InfoIcon" :size="16" class="text-secondary cursor-help" />
45 + </template>
46 + <div class="text-sm">Click on a segment to go to the event search page.</div>
47 + </n-tooltip>
48 + </template>
49 +
50 + <div v-if="item.panel.type === 'stat'" class="font-mono text-2xl font-semibold">
51 + {{ formatCompactNumber(item.data?.value) }}
52 + </div>
53 +
54 + <component
55 + :is="chartByType[item.panel.type]"
56 + v-if="item.data && chartByType[item.panel.type]"
57 + :labels="item.data.labels"
58 + :data="item.data.data"
59 + :monochrome="item.panel.type === 'histogram'"
60 + :labels-datetime="item.panel.type === 'histogram'"
61 + :height="`${item.panel.type === 'histogram' ? `${item.panel.h + 100}px` : `${item.panel.h}px`}`"
62 + @item-click="onChartItemClick(item.panel, $event.name)"
63 + />
64 +
65 + <span v-if="item.data?.error" class="text-error text-xs">
66 + {{ item.data.error }}
67 + </span>
68 + </CardLink>
69 + </n-spin>
70 +
71 + <n-empty v-if="!loading && !hasData && errorMsg" :description="errorMsg" />
72 + </div>
73 +</template>
74 +
75 +<script setup lang="ts">
76 +import type { Component } from "vue"
77 +import type { ApiError } from "@/types/common"
78 +import type { DashboardPanel, DashboardPanelType, PanelResult } from "@/types/dashboards.d"
79 +import axios from "axios"
80 +import { NButton, NEmpty, NRadioButton, NRadioGroup, NSpin, NTooltip, useMessage } from "naive-ui"
81 +import { computed, ref, watch } from "vue"
82 +import { useRouter } from "vue-router"
83 +import CardLink from "@/components/common/cards/CardLink.vue"
84 +import ChartBar from "@/components/common/charts/ChartBar.vue"
85 +import ChartColumn from "@/components/common/charts/ChartColumn.vue"
86 +import ChartPie from "@/components/common/charts/ChartPie.vue"
87 +import Icon from "@/components/common/Icon.vue"
88 +import { formatCompactNumber } from "@/utils"
89 +import { panelColSpanClass } from "./utils"
90 +import { SiemAPI } from "@/api/siem"
91 +
92 +const { dashboardId } = defineProps<{
93 + dashboardId: number
94 +}>()
95 +
96 +const ArrowBackIcon = "carbon:arrow-left"
97 +const RefreshIcon = "carbon:renew"
98 +
99 +interface DashboardPanelEntry {
100 + panel: DashboardPanel
101 + data: PanelResult | undefined
102 +}
103 +
104 +const chartByType: Record<DashboardPanelType, Component | undefined> = {
105 + stat: undefined,
106 + pie: ChartPie,
107 + bar_h: ChartBar,
108 + histogram: ChartColumn
109 +}
110 +
111 +const router = useRouter()
112 +const InfoIcon = "carbon:information"
113 +const message = useMessage()
114 +
115 +const timePresets = ["1h", "6h", "24h", "7d", "30d"]
116 +
117 +const dashboardTitle = ref("")
118 +const dashboardDescription = ref("")
119 +const customerCode = ref("")
120 +const sourceName = ref("")
121 +const panels = ref<DashboardPanelEntry[]>([])
122 +const loading = ref(false)
123 +const errorMsg = ref("")
124 +const selectedTimerange = ref(timePresets[2])
125 +
126 +const hasData = computed(() => panels.value.some(row => row.data != null))
127 +
128 +let abortController = new AbortController()
129 +
130 +async function fetchPanelData() {
131 + if (abortController) {
132 + abortController.abort()
133 + }
134 +
135 + abortController = new AbortController()
136 +
137 + loading.value = true
138 + errorMsg.value = ""
139 +
140 + try {
141 + const res = await SiemAPI.getPanelData(dashboardId, selectedTimerange.value, abortController.signal)
142 +
143 + if (res.success) {
144 + dashboardTitle.value = res.template.title
145 + dashboardDescription.value = res.template.description
146 +
147 + panels.value = res.template.panels.map(p => ({
148 + panel: p,
149 + data: res.panels[p.id]
150 + }))
151 +
152 + customerCode.value = res.customer_code
153 + sourceName.value = res.source_name
154 + } else {
155 + errorMsg.value = res.message || "Failed to fetch panel data"
156 + message.error(errorMsg.value)
157 + }
158 +
159 + loading.value = false
160 + } catch (error) {
161 + if (!axios.isCancel(error)) {
162 + loading.value = false
163 + errorMsg.value = (error as ApiError).response?.data?.message || "Failed to fetch panel data"
164 + message.error(errorMsg.value)
165 + }
166 + }
167 +}
168 +
169 +function openEventSearch(luceneQuery: string) {
170 + const routeData = router.resolve({
171 + name: "EventSearch",
172 + query: {
173 + customer_code: customerCode.value,
174 + source_name: sourceName.value,
175 + query: luceneQuery
176 + }
177 + })
178 + window.open(routeData.href, "_blank")
179 +}
180 +
181 +function buildDrillDownQuery(panel: DashboardPanel, clickedValue: string): string {
182 + const baseLucene = panel.lucene && panel.lucene !== "*" ? `(${panel.lucene})` : ""
183 + const fieldFilter = panel.field ? `${panel.field}:"${clickedValue}"` : ""
184 + return [baseLucene, fieldFilter].filter(Boolean).join(" AND ")
185 +}
186 +
187 +function onChartItemClick(panel: DashboardPanel, name: string) {
188 + if (panel.type === "histogram") {
189 + return
190 + }
191 + const query = buildDrillDownQuery(panel, name)
192 + if (query) openEventSearch(query)
193 +}
194 +
195 +watch(
196 + selectedTimerange,
197 + () => {
198 + fetchPanelData()
199 + },
200 + { immediate: true }
201 +)
202 +</script>
customer_portal/src/components/dashboards/DashboardsBrowser.vue new
+132
@@ -0,0 +1,132 @@
1 +<template>
2 + <div class="flex flex-col gap-4">
3 + <!-- Filters Bar -->
4 + <n-form-item label="Customer" :show-feedback="false">
5 + <n-select
6 + v-model:value="selectedCustomerCode"
7 + :options="customersOptions"
8 + placeholder="Select Customer"
9 + filterable
10 + :loading="loadingCustomers"
11 + :consistent-menu-width="false"
12 + clearable
13 + />
14 + </n-form-item>
15 +
16 + <!-- No Event Sources Warning -->
17 + <n-alert v-if="showNoSourcesWarning" title="No Event Sources Configured" type="warning">
18 + An Event Source needs to be defined for this customer before dashboards can be enabled. Go to the customer's
19 + <strong>Event Sources</strong>
20 + tab to configure one.
21 + </n-alert>
22 +
23 + <EnabledDashboardsSection
24 + ref="enabledDashboardsSectionRef"
25 + v-model:enabled-dashboards="enabledDashboards"
26 + :customer-code="selectedCustomerCode"
27 + :visible="!!selectedCustomerCode && !showNoSourcesWarning"
28 + :event-sources-list
29 + />
30 +
31 + <DashboardCategoriesSection
32 + :selected-customer-code
33 + :event-sources-list
34 + :loading-event-sources
35 + :enabled-dashboards
36 + @refresh-enabled-dashboards="refreshEnabledDashboards"
37 + />
38 + </div>
39 +</template>
40 +
41 +<script setup lang="ts">
42 +import type { Customer } from "@/types/customers.d"
43 +import type { EnabledDashboard } from "@/types/dashboards.d"
44 +import type { EventSource } from "@/types/eventSources.d"
45 +import { NAlert, NFormItem, NSelect, useMessage } from "naive-ui"
46 +import { computed, onBeforeMount, ref, useTemplateRef, watch } from "vue"
47 +import Api from "@/api"
48 +import DashboardCategoriesSection from "./DashboardCategoriesSection.vue"
49 +import EnabledDashboardsSection from "./EnabledDashboardsSection.vue"
50 +
51 +const message = useMessage()
52 +
53 +// ── Customer selection ──────────────────────────────────────────
54 +const loadingCustomers = ref(false)
55 +const customersList = ref<Customer[]>([])
56 +const selectedCustomerCode = ref<string | null>(null)
57 +
58 +// ── Enabled dashboards (lista aggiornata da EnabledDashboardsSection via v-model) ──
59 +const enabledDashboards = ref<EnabledDashboard[]>([])
60 +const enabledDashboardsSectionRef = useTemplateRef<InstanceType<typeof EnabledDashboardsSection>>(
61 + "enabledDashboardsSectionRef"
62 +)
63 +
64 +const customersOptions = computed(() =>
65 + customersList.value.map(c => ({ label: `#${c.customer_code} - ${c.customer_name}`, value: c.customer_code }))
66 +)
67 +
68 +function getCustomers() {
69 + loadingCustomers.value = true
70 +
71 + Api.customers
72 + .getCustomers()
73 + .then(res => {
74 + if (res.data.success) {
75 + customersList.value = res.data?.customers || []
76 + } else {
77 + message.warning(res.data?.message || "An error occurred. Please try again later.")
78 + }
79 + })
80 + .catch(err => {
81 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
82 + })
83 + .finally(() => {
84 + loadingCustomers.value = false
85 + })
86 +}
87 +
88 +// ── Event Source selection ───────────────────────────────────────
89 +const loadingEventSources = ref(false)
90 +const eventSourcesList = ref<EventSource[]>([])
91 +
92 +const showNoSourcesWarning = computed(
93 + () => selectedCustomerCode.value && !loadingEventSources.value && !eventSourcesList.value.length
94 +)
95 +
96 +function getEventSources(customerCode: string) {
97 + loadingEventSources.value = true
98 + eventSourcesList.value = []
99 +
100 + Api.siem
101 + .getEventSources(customerCode)
102 + .then(res => {
103 + if (res.data.success) {
104 + eventSourcesList.value = res.data?.event_sources || []
105 + } else {
106 + message.warning(res.data?.message || "An error occurred. Please try again later.")
107 + }
108 + })
109 + .catch(err => {
110 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
111 + })
112 + .finally(() => {
113 + loadingEventSources.value = false
114 + })
115 +}
116 +
117 +function refreshEnabledDashboards() {
118 + enabledDashboardsSectionRef.value?.refreshEnabledDashboards()
119 +}
120 +
121 +watch(selectedCustomerCode, code => {
122 + eventSourcesList.value = []
123 +
124 + if (code) {
125 + getEventSources(code)
126 + }
127 +})
128 +
129 +onBeforeMount(() => {
130 + getCustomers()
131 +})
132 +</script>
customer_portal/src/components/dashboards/EnabledDashboardsSection.vue new
+176
@@ -0,0 +1,176 @@
1 +<template>
2 + <n-card v-if="visible" ref="wrapperRef" size="small">
3 + <template #header>Enabled Dashboards</template>
4 + <template #header-extra>
5 + <span class="text-secondary text-sm">{{ enabledDashboards.length }} enabled</span>
6 + </template>
7 +
8 + <n-data-table
9 + bordered
10 + :loading="loadingEnabled"
11 + size="small"
12 + :data="enabledDashboards"
13 + :columns="enabledColumns"
14 + :scroll-x="600"
15 + :pagination="false"
16 + class="[&_.n-data-table-th\_\_title]:whitespace-nowrap"
17 + >
18 + <template #empty>
19 + <n-empty description="No enabled dashboards" />
20 + </template>
21 + </n-data-table>
22 + </n-card>
23 +</template>
24 +
25 +<script setup lang="ts">
26 +import type { DataTableColumns } from "naive-ui"
27 +import type { EnabledDashboard } from "@/types/dashboards.d"
28 +import type { EventSource } from "@/types/eventSources.d"
29 +import { useElementSize } from "@vueuse/core"
30 +import { NButton, NCard, NDataTable, NEmpty, useDialog, useMessage } from "naive-ui"
31 +import { computed, h, ref, useTemplateRef, watch } from "vue"
32 +import { useRouter } from "vue-router"
33 +import Api from "@/api"
34 +
35 +const props = defineProps<{
36 + customerCode: string | null
37 + visible: boolean
38 + eventSourcesList: EventSource[]
39 +}>()
40 +
41 +const enabledDashboards = defineModel<EnabledDashboard[]>("enabledDashboards", { default: () => [] })
42 +
43 +const loadingEnabled = ref(false)
44 +
45 +const message = useMessage()
46 +const dialog = useDialog()
47 +const { width: headerWidthRef } = useElementSize(useTemplateRef("wrapperRef"))
48 +const router = useRouter()
49 +const simpleMode = computed(() => headerWidthRef.value < 600)
50 +
51 +function fetchEnabledDashboards(customerCode: string) {
52 + loadingEnabled.value = true
53 +
54 + Api.siem
55 + .getEnabledDashboards(customerCode)
56 + .then(res => {
57 + if (res.data.success) {
58 + enabledDashboards.value = res.data?.enabled_dashboards || []
59 + } else {
60 + message.warning(res.data?.message || "An error occurred. Please try again later.")
61 + }
62 + })
63 + .catch(err => {
64 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
65 + })
66 + .finally(() => {
67 + loadingEnabled.value = false
68 + })
69 +}
70 +
71 +function refreshEnabledDashboards() {
72 + const code = props.customerCode
73 + if (code) {
74 + fetchEnabledDashboards(code)
75 + }
76 +}
77 +
78 +watch(
79 + () => props.customerCode,
80 + code => {
81 + if (!code) {
82 + loadingEnabled.value = false
83 + enabledDashboards.value = []
84 + return
85 + }
86 + fetchEnabledDashboards(code)
87 + },
88 + { immediate: true }
89 +)
90 +
91 +defineExpose({
92 + refreshEnabledDashboards
93 +})
94 +
95 +const enabledColumns = computed<DataTableColumns<EnabledDashboard>>(() => [
96 + { title: "Display Name", key: "display_name", minWidth: 240 },
97 + { title: "Category", key: "library_card", width: 150 },
98 + { title: "Template", key: "template_id", width: 180 },
99 + {
100 + title: "Event Source",
101 + key: "event_source_id",
102 + width: 180,
103 + render(row) {
104 + const source = props.eventSourcesList.find(s => s.id === row.event_source_id)
105 + return source ? `${source.name} (${source.event_type})` : `#${row.event_source_id}`
106 + }
107 + },
108 + {
109 + title: "Created",
110 + key: "created_at",
111 + width: 180,
112 + render(row) {
113 + return new Date(row.created_at).toLocaleString()
114 + }
115 + },
116 + {
117 + title: "",
118 + key: "actions",
119 + width: 160,
120 + fixed: simpleMode.value ? undefined : "right",
121 + render(row) {
122 + return h("div", { class: "flex gap-2" }, [
123 + h(
124 + NButton,
125 + {
126 + size: "small",
127 + type: "primary",
128 + quaternary: true,
129 + onClick: () => {
130 + router.push({ name: "DashboardView", params: { id: String(row.id) } })
131 + }
132 + },
133 + { default: () => "View" }
134 + ),
135 + h(
136 + NButton,
137 + {
138 + size: "small",
139 + type: "error",
140 + quaternary: true,
141 + onClick: () => {
142 + dialog.warning({
143 + title: "Disable Dashboard",
144 + content: `Are you sure you want to disable "${row.display_name}"?`,
145 + positiveText: "Disable",
146 + negativeText: "Cancel",
147 + onPositiveClick: () => {
148 + Api.siem
149 + .disableDashboard(row.id)
150 + .then(res => {
151 + if (res.data.success) {
152 + message.success("Dashboard disabled successfully")
153 + refreshEnabledDashboards()
154 + } else {
155 + message.warning(
156 + res.data?.message || "An error occurred. Please try again later."
157 + )
158 + }
159 + })
160 + .catch(err => {
161 + message.error(
162 + err.response?.data?.message ||
163 + "An error occurred. Please try again later."
164 + )
165 + })
166 + }
167 + })
168 + }
169 + },
170 + { default: () => "Disable" }
171 + )
172 + ])
173 + }
174 + }
175 +])
176 +</script>
customer_portal/src/components/dashboards/utils.ts new
+37
@@ -0,0 +1,37 @@
1 +export function getDashboardIcon(icon: string) {
2 + switch (icon) {
3 + case "cpu":
4 + case "memory":
5 + return "carbon:chip"
6 + case "network":
7 + return "carbon:load-balancer-network"
8 + case "storage":
9 + return "carbon:vmdk-disk"
10 + case "security":
11 + return "carbon:security"
12 + case "performance":
13 + return "ph:gauge"
14 + default:
15 + return "carbon:dashboard"
16 + }
17 +}
18 +
19 +/** Span responsive su griglia 12 col: `col-span-12` su schermi stretti, poi breakpoint (`@lg`, `@2xl`, `@3xl`, `@5xl`) verso lo span target `w`. */
20 +export const PANEL_COL_SPAN_BY_WIDTH: Record<number, string> = {
21 + 1: "col-span-12 @2xl:col-span-4 @3xl:col-span-2 @5xl:col-span-1",
22 + 2: "col-span-12 @lg:col-span-4 @5xl:col-span-2",
23 + 3: "col-span-12 @lg:col-span-6 @5xl:col-span-3",
24 + 4: "col-span-12 @4xl:col-span-4",
25 + 5: "col-span-12 @3xl:col-span-5",
26 + 6: "col-span-12 @2xl:col-span-6",
27 + 7: "col-span-12 @3xl:col-span-7",
28 + 8: "col-span-12 @2xl:col-span-8",
29 + 9: "col-span-12 @2xl:col-span-9",
30 + 10: "col-span-12 @xl:col-span-10",
31 + 11: "col-span-12 @xl:col-span-11",
32 + 12: "col-span-12"
33 +}
34 +
35 +export function panelColSpanClass(w: number): string {
36 + return PANEL_COL_SPAN_BY_WIDTH[w] ?? "col-span-12"
37 +}
customer_portal/src/composables/useBreadcrumb.ts new
+89
@@ -0,0 +1,89 @@
1 +import { computed, ref } from "vue"
2 +
3 +export interface BreadcrumbItem {
4 + name: string
5 + path: string
6 + key: string
7 +}
8 +
9 +const breadcrumbItems = ref<BreadcrumbItem[]>([])
10 +
11 +export function useBreadcrumb() {
12 + const items = computed(() => breadcrumbItems.value)
13 +
14 + /**
15 + * Adds a new item to the end of the breadcrumb
16 + */
17 + function push(item: BreadcrumbItem) {
18 + breadcrumbItems.value.push(item)
19 + }
20 +
21 + /**
22 + * Replaces the last breadcrumb item
23 + * If there are no items, adds the new item
24 + */
25 + function replace(item: BreadcrumbItem) {
26 + if (breadcrumbItems.value.length === 0) {
27 + breadcrumbItems.value.push(item)
28 + } else {
29 + breadcrumbItems.value[breadcrumbItems.value.length - 1] = item
30 + }
31 + }
32 +
33 + /**
34 + * Updates only the name of the last breadcrumb item
35 + * If there are no items, does nothing
36 + */
37 + function updateLastCrumbName(name: string) {
38 + if (breadcrumbItems.value.length > 0) {
39 + const lastItem = breadcrumbItems.value.at(-1)
40 + if (!lastItem) return
41 +
42 + breadcrumbItems.value[breadcrumbItems.value.length - 1] = {
43 + ...lastItem,
44 + name
45 + }
46 + }
47 + }
48 +
49 + /**
50 + * Removes the last breadcrumb item
51 + */
52 + function pop() {
53 + return breadcrumbItems.value.pop()
54 + }
55 +
56 + /**
57 + * Clears all breadcrumb items
58 + */
59 + function clear() {
60 + breadcrumbItems.value = []
61 + }
62 +
63 + /**
64 + * Sets all breadcrumb items
65 + */
66 + function setItems(items: BreadcrumbItem[]) {
67 + breadcrumbItems.value = items
68 + }
69 +
70 + /**
71 + * Removes items until reaching the specified index
72 + */
73 + function truncate(index: number) {
74 + if (index >= 0 && index < breadcrumbItems.value.length) {
75 + breadcrumbItems.value = breadcrumbItems.value.slice(0, index + 1)
76 + }
77 + }
78 +
79 + return {
80 + items,
81 + push,
82 + replace,
83 + updateLastCrumbName,
84 + pop,
85 + clear,
86 + setItems,
87 + truncate
88 + }
89 +}
customer_portal/src/composables/useCustomerHealthcheckFilters.ts new
+12
@@ -0,0 +1,12 @@
1 +import { ref } from "vue"
2 +
3 +const healthcheckFilters = ref<Partial<{ time: number; unit: "minutes" | "hours" | "days" }>>({
4 + time: 1,
5 + unit: "days"
6 +})
7 +
8 +export function useCustomerHealthcheckFilters() {
9 + return {
10 + healthcheckFilters
11 + }
12 +}
customer_portal/src/composables/useFullscreenSwitch.ts new
+12
@@ -0,0 +1,12 @@
1 +import { useFullscreen } from "@vueuse/core"
2 +
3 +const { toggle, isFullscreen } = useFullscreen()
4 +
5 +export function useFullscreenSwitch() {
6 + return {
7 + toggle: () => {
8 + toggle()
9 + },
10 + isFullscreen
11 + }
12 +}
customer_portal/src/composables/useGlobalActions.ts new
+30
@@ -0,0 +1,30 @@
1 +import type { MessageOptions } from "naive-ui"
2 +import type { MessageApiInjection, MessageReactive } from "naive-ui/es/message/src/MessageProvider"
3 +import type { NotificationOptions } from "naive-ui/es/notification/src/NotificationEnvironment"
4 +import type { NotificationApiInjection, NotificationReactive } from "naive-ui/es/notification/src/NotificationProvider"
5 +import type { Writable } from "type-fest"
6 +
7 +export type NotificationObject = Writable<NotificationOptions>
8 +
9 +interface InitPayload {
10 + message: MessageApiInjection
11 + notification: NotificationApiInjection
12 +}
13 +
14 +let message: MessageApiInjection | null = null
15 +let notification: NotificationApiInjection | null = null
16 +
17 +export function useGlobalActions() {
18 + return {
19 + init: (payload: InitPayload): void => {
20 + message = payload.message
21 + notification = payload.notification
22 + },
23 + message: (content: string, options?: MessageOptions): MessageReactive | undefined => {
24 + return message?.create(content, options || { type: "info" })
25 + },
26 + notification: (options: NotificationOptions): NotificationReactive | undefined => {
27 + return notification?.create(options)
28 + }
29 + }
30 +}
customer_portal/src/composables/useHealthchecksNotify.ts new
+103
@@ -0,0 +1,103 @@
1 +import type { Notification } from "./useNotifications"
2 +import _capitalize from "lodash/capitalize"
3 +import { computed, watch } from "vue"
4 +import { useHealthcheckStore } from "@/stores/healthcheck"
5 +import { IndexHealth } from "@/types/indices.d"
6 +import { useNavigation } from "./useNavigation"
7 +import { useNotifications } from "./useNotifications"
8 +
9 +export function useHealthchecksNotify() {
10 + return {
11 + init: () => {
12 + const { routeHealthcheck, routeIndex, routeGraylogMetrics } = useNavigation()
13 +
14 + const uncommittedJournalEntriesThreshold = useHealthcheckStore().uncommittedJournalEntriesThreshold
15 + const uncommittedJournalEntries = computed(() => useHealthcheckStore().uncommittedJournalEntries)
16 + const clusterName = computed(() => useHealthcheckStore().clusterName)
17 + const clusterStatus = computed(() => useHealthcheckStore().clusterStatus)
18 + const alerts = computed(() => useHealthcheckStore().alerts)
19 +
20 + useHealthcheckStore().start()
21 +
22 + watch(uncommittedJournalEntries, (val, old) => {
23 + if (val !== old) {
24 + const obj: Notification = {
25 + id: "uncommittedJournalEntries",
26 + category: "alert",
27 + type: "error",
28 + title: "Error check",
29 + description: "Uncommitted Journal Entries",
30 + read: false,
31 + date: new Date(),
32 + action() {
33 + routeGraylogMetrics().navigate()
34 + },
35 + actionTitle: "See Graylog Metrics"
36 + }
37 +
38 + if (val !== null && val >= uncommittedJournalEntriesThreshold) {
39 + obj.type = "warning"
40 + obj.title = "Uncommitted Journal Entries"
41 + obj.description = `Value ${val} (over ${uncommittedJournalEntriesThreshold})`
42 + }
43 +
44 + useNotifications().prepend(obj, { autoNotify: true })
45 + }
46 + })
47 +
48 + watch(clusterStatus, (val, old) => {
49 + if (val !== old) {
50 + const obj: Notification = {
51 + id: "clusterHealth",
52 + category: "alert",
53 + type: "error",
54 + title: "Error check",
55 + description: "Cluster Health",
56 + read: false,
57 + date: new Date(),
58 + action() {
59 + routeIndex().navigate()
60 + },
61 + actionTitle: "See Cluster"
62 + }
63 +
64 + if (val !== null && val !== IndexHealth.GREEN) {
65 + obj.type = val === IndexHealth.YELLOW ? "warning" : "error"
66 + obj.title = "Cluster Health"
67 + obj.description = `${_capitalize(clusterName.value || "Cluster")} is ${val.toUpperCase()}`
68 + }
69 +
70 + useNotifications().prepend(obj, { autoNotify: true })
71 + }
72 + })
73 +
74 + watch(
75 + alerts,
76 + (val, old) => {
77 + if (JSON.stringify(val) !== JSON.stringify(old)) {
78 + if (val !== null && val.length) {
79 + const obj: Notification = {
80 + id: "influxDBAlert",
81 + category: "alert",
82 + type: "warning",
83 + title: "Influx Alert",
84 + description: `${val.length} Critical ${val.length > 1 ? "issues" : "issue"}`,
85 + read: false,
86 + date: new Date(),
87 + action() {
88 + routeHealthcheck().navigate()
89 + },
90 + actionTitle: "See Healthcheck"
91 + }
92 +
93 + useNotifications().prepend(obj, { autoNotify: true })
94 + } else {
95 + useNotifications().deleteOne("influxDBAlert")
96 + }
97 + }
98 + },
99 + { deep: true }
100 + )
101 + }
102 + }
103 +}
customer_portal/src/composables/useHideLayoutFooter.ts new
+19
@@ -0,0 +1,19 @@
1 +import { onBeforeMount, onBeforeUnmount } from "vue"
2 +import { useThemeStore } from "@/stores/theme"
3 +
4 +// :has() CSS relational pseudo-class not yet supported by Firefox
5 +// (https://caniuse.com/css-has)
6 +// at the moment this worker around permit to hide Layout Footer
7 +
8 +export function useHideLayoutFooter() {
9 + const themeStore = useThemeStore()
10 +
11 + if (themeStore.isFooterShown) {
12 + onBeforeMount(() => {
13 + themeStore.setFooterShow(false)
14 + })
15 + onBeforeUnmount(() => {
16 + themeStore.setFooterShow(true)
17 + })
18 + }
19 +}
customer_portal/src/composables/useLoadingBarSetup.ts new
+20
@@ -0,0 +1,20 @@
1 +import { useLoadingBar } from "naive-ui"
2 +import { onMounted } from "vue"
3 +import { useRouter } from "vue-router"
4 +import { useMainStore } from "@/stores/main"
5 +
6 +export function useLoadingBarSetup() {
7 + const router = useRouter()
8 + const mainStore = useMainStore()
9 +
10 + onMounted(() => {
11 + const loadingBar = useLoadingBar()
12 + router.beforeEach((to, current) => {
13 + if (to.path !== current.path) {
14 + loadingBar?.start()
15 + }
16 + })
17 + router.afterEach(() => loadingBar?.finish())
18 + mainStore.setLoadingBar(loadingBar)
19 + })
20 +}
customer_portal/src/composables/useNavigation.ts new
+31
@@ -0,0 +1,31 @@
1 +import type { RouteLocationRaw } from "vue-router"
2 +import { useRouter } from "vue-router"
3 +
4 +export function useNavigation() {
5 + const router = useRouter()
6 +
7 + function routerConstructor(route: RouteLocationRaw) {
8 + return {
9 + navigate: () => router.push(route),
10 + replace: () => router.replace(route),
11 + valueOf: () => router.resolve(route).href,
12 + toString: () => router.resolve(route).href,
13 + fullUrl: () => {
14 + const resolved = router.resolve(route)
15 + return `${window.location.protocol}//${window.location.host}${resolved.href}`
16 + }
17 + }
18 + }
19 +
20 + function routeEventSearch(params?: { customer_code?: string; source_name?: string; query?: string }) {
21 + const routeQuery: Record<string, string> = {}
22 + if (params?.customer_code) routeQuery.customer_code = params.customer_code
23 + if (params?.source_name) routeQuery.source_name = params.source_name
24 + if (params?.query) routeQuery.query = params.query
25 + return routerConstructor({ name: "EventSearch", query: routeQuery })
26 + }
27 +
28 + return {
29 + routeEventSearch
30 + }
31 +}
customer_portal/src/composables/useNotifications.ts new
+117
@@ -0,0 +1,117 @@
1 +import type { NotificationObject } from "./useGlobalActions"
2 +import { useStorage } from "@vueuse/core"
3 +import _uniqBy from "lodash/uniqBy"
4 +import { NButton } from "naive-ui"
5 +import { computed, h } from "vue"
6 +import { useSettingsStore } from "@/stores/settings"
7 +import dayjs from "@/utils/dayjs"
8 +import { secureLocalStorage } from "@/utils/secure-storage"
9 +import { useGlobalActions } from "./useGlobalActions"
10 +
11 +export type NotificationCategory = "alert"
12 +export type NotificationType = "success" | "info" | "warning" | "error" | "default" | undefined
13 +export interface Notification {
14 + id: number | string
15 + category: NotificationCategory
16 + type: NotificationType
17 + title: string
18 + description: string
19 + read: boolean
20 + date: string | Date
21 + action?: () => void
22 + actionTitle?: string
23 +}
24 +
25 +export interface PrependOptions {
26 + /** prepend and send a notification */
27 + sendNotify?: boolean
28 + /** send a notification only if there isn't any item with match id/type/category */
29 + autoNotify?: boolean
30 +}
31 +
32 +const list = useStorage<Notification[]>("notifications-list", [], secureLocalStorage({ session: true }))
33 +
34 +export function useNotifications() {
35 + const hasUnread = computed(() => list.value.filter(o => !o.read).length !== 0)
36 + const hasNotifications = computed(() => list.value.length !== 0)
37 + const dFormats = useSettingsStore().dateFormat
38 +
39 + function formatDatetime(date: Date | string) {
40 + const datejs = dayjs(date)
41 + if (!datejs.isValid()) return date
42 +
43 + if (dayjs().isSame(datejs, "day")) {
44 + return datejs.format(dFormats.time)
45 + }
46 + return datejs.format("D MMM")
47 + }
48 +
49 + return {
50 + list,
51 + hasUnread,
52 + hasNotifications,
53 + formatDatetime,
54 + setRead: (id: string | number) => {
55 + const item = list.value.find(o => o.id === id)
56 + if (item) {
57 + item.read = true
58 + }
59 + },
60 + setAllRead: () => {
61 + for (const item of list.value) {
62 + item.read = true
63 + }
64 + },
65 + deleteOne: (id: string | number) => {
66 + list.value = list.value.filter(o => o.id !== id)
67 + },
68 + deleteAll: () => {
69 + list.value = []
70 + },
71 + prepend: (newItem: Notification, options?: PrependOptions) => {
72 + let sendNotify = options?.sendNotify || false
73 + const autoNotify = options?.autoNotify || false
74 +
75 + if (autoNotify) {
76 + const item = list.value.find(
77 + o => o.id === newItem.id && o.type === newItem.type && o.category === newItem.category
78 + )
79 +
80 + if (!item) {
81 + sendNotify = true
82 + }
83 + }
84 +
85 + if (sendNotify) {
86 + const notify: NotificationObject = {
87 + title: newItem.title,
88 + content: newItem.description,
89 + type: newItem.type,
90 + meta: formatDatetime(newItem.date).toString(),
91 + action: undefined,
92 + duration: 3000,
93 + keepAliveOnHover: true
94 + }
95 +
96 + if (newItem.action) {
97 + notify.action = () =>
98 + h(
99 + NButton,
100 + {
101 + text: true,
102 + type: newItem.type,
103 + onClick: newItem.action
104 + },
105 + {
106 + default: () => newItem.actionTitle || "Details"
107 + }
108 + )
109 + }
110 +
111 + useGlobalActions().notification(notify)
112 + }
113 +
114 + list.value = _uniqBy([newItem, ...list.value], o => o.id)
115 + }
116 + }
117 +}
customer_portal/src/composables/useSearchDialog.ts new
+13
@@ -0,0 +1,13 @@
1 +import { ref } from "vue"
2 +
3 +const listener = ref()
4 +export function useSearchDialog() {
5 + return {
6 + trigger: (cb: () => void): void => {
7 + listener.value = cb
8 + },
9 + open: (): void => {
10 + listener.value?.()
11 + }
12 + }
13 +}
customer_portal/src/composables/useThemeSwitch.ts new
+11
@@ -0,0 +1,11 @@
1 +import { useThemeStore } from "@/stores/theme"
2 +
3 +export function useThemeSwitch() {
4 + const themeStore = useThemeStore()
5 +
6 + return {
7 + toggle: () => {
8 + themeStore.toggleTheme()
9 + }
10 + }
11 +}
customer_portal/src/design-tokens.json new
+108
@@ -0,0 +1,108 @@
1 +{
2 + "borderRadius": {
3 + "default": "8px",
4 + "small": "4px"
5 + },
6 + "lineHeight": {
7 + "default": "1.25"
8 + },
9 + "fontSize": {
10 + "default": "16px",
11 + "cardTitle": "18px"
12 + },
13 + "fontWeight": {
14 + "default": "400",
15 + "strong": "600",
16 + "cardTitle": "600"
17 + },
18 + "fontFamily": {
19 + "default": "'Public Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'",
20 + "display": "'Lexend', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'",
21 + "mono": "'JetBrains Mono', SFMono-Regular, Menlo, Consolas, Courier, monospace"
22 + },
23 + "typography": {
24 + "h1": {
25 + "fontFamily": "{fontFamily.display}",
26 + "fontSize": "30px",
27 + "fontWeight": "700",
28 + "lineHeight": "38"
29 + },
30 + "h2": {
31 + "fontFamily": "{fontFamily.display}",
32 + "fontSize": "26px",
33 + "fontWeight": "700",
34 + "lineHeight": "33"
35 + },
36 + "h3": {
37 + "fontFamily": "{fontFamily.display}",
38 + "fontSize": "22px",
39 + "fontWeight": "700",
40 + "lineHeight": "28"
41 + },
42 + "h4": {
43 + "fontFamily": "{fontFamily.display}",
44 + "fontSize": "18px",
45 + "fontWeight": "500",
46 + "lineHeight": "23"
47 + },
48 + "h5": {
49 + "fontFamily": "{fontFamily.display}",
50 + "fontSize": "14px",
51 + "fontWeight": "700",
52 + "lineHeight": "18"
53 + },
54 + "h6": {
55 + "fontFamily": "{fontFamily.default}",
56 + "fontSize": "12px",
57 + "fontWeight": "500",
58 + "lineHeight": "15"
59 + },
60 + "p": {
61 + "fontFamily": "{fontFamily.default}",
62 + "fontSize": "{fontSize.default}",
63 + "lineHeight": "20"
64 + }
65 + },
66 + "colors": {
67 + "light": {
68 + "sidebarBackground": "rgb(255, 255, 255)",
69 + "bodyBackground": "rgb(245, 247, 249)",
70 + "background": "rgb(255, 255, 255)",
71 + "backgroundSecondary": "rgb(250, 251, 252)",
72 + "text": "rgb(0, 0, 0)",
73 + "textSecondary": "rgb(73, 84, 101)",
74 + "textTertiary": "rgb(114, 115, 115)",
75 + "border": "rgb(226, 230, 233)",
76 + "hover": "rgba(221, 224, 225, 0.5)",
77 + "primary": "rgb(219, 157, 0)",
78 + "info": "rgb(98, 103, 255)",
79 + "success": "rgb(0, 178, 123)",
80 + "warning": "rgb(227, 194, 47)",
81 + "error": "rgb(255, 1, 86)",
82 + "extra1": "rgb(98, 103, 255)",
83 + "extra2": "rgb(255, 97, 201)",
84 + "extra3": "rgb(227, 194, 47)",
85 + "extra4": "rgb(255, 1, 86)"
86 + },
87 + "dark": {
88 + "sidebarBackground": "rgb(29, 31, 37)",
89 + "bodyBackground": "rgb(20, 22, 26)",
90 + "background": "rgb(38, 40, 45)",
91 + "backgroundSecondary": "rgb(29, 31, 37)",
92 + "text": "rgb(255, 255, 255)",
93 + "textSecondary": "rgb(172, 181, 190)",
94 + "textTertiary": "rgb(116, 126, 136)",
95 + "border": "rgb(67, 68, 76)",
96 + "hover": "rgba(59, 61, 68, 0.5)",
97 + "primary": "rgb(255, 182, 0)",
98 + "info": "rgb(98, 103, 255)",
99 + "success": "rgb(0, 225, 155)",
100 + "warning": "rgb(227, 194, 47)",
101 + "error": "rgb(255, 1, 86)",
102 + "extra1": "rgb(98, 103, 255)",
103 + "extra2": "rgb(255, 97, 201)",
104 + "extra3": "rgb(227, 194, 47)",
105 + "extra4": "rgb(255, 1, 86)"
106 + }
107 + }
108 +}
customer_portal/src/lang/config.ts new
+22
@@ -0,0 +1,22 @@
1 +import type { I18nOptions } from "vue-i18n"
2 +import type { RecursiveKeyOf } from "@/types/common"
3 +import * as locales from "./locales"
4 +
5 +export type LocaleCodes = keyof typeof locales
6 +export type MessageSchema = (typeof locales)[LocaleCodes]
7 +export type I18nString = RecursiveKeyOf<MessageSchema>
8 +
9 +export function getI18NConf() {
10 + const localesEntries = Object.entries<MessageSchema>(locales)
11 +
12 + const messages = localesEntries.reduce((acc: { [key: string]: MessageSchema }, cur: [string, MessageSchema]) => {
13 + acc[cur[0]] = cur[1]
14 + return acc
15 + }, {}) as { [key in LocaleCodes]: MessageSchema }
16 +
17 + return {
18 + legacy: false,
19 + locale: "en",
20 + messages
21 + } satisfies I18nOptions
22 +}
customer_portal/src/lang/index.ts new
+9
@@ -0,0 +1,9 @@
1 +import type { LocaleCodes, MessageSchema } from "./config"
2 +import { createI18n } from "vue-i18n"
3 +import { getI18NConf } from "./config"
4 +
5 +const instance = createI18n<[MessageSchema], LocaleCodes>(getI18NConf())
6 +
7 +export default instance
8 +
9 +export const i18nGlobal = instance.global
customer_portal/src/lang/locales/de.json new
+11
@@ -0,0 +1,11 @@
1 +{
2 + "locales": {
3 + "it": "Italienisch",
4 + "en": "Englisch",
5 + "jp": "Japanisch",
6 + "fr": "Französisch",
7 + "de": "Deutsch",
8 + "es": "Spanisch"
9 + },
10 + "sun_description": "Die Sonne ist ein mittelgroßer Stern, der sich im Zentrum unseres Sonnensystems befindet. Sie ist eine Quelle von Licht und Wärme, die das Leben auf der Erde ermöglicht. Hauptsächlich aus Wasserstoff und Helium bestehend, erzeugt sie Energie durch Kernfusion. Ihre Masse und Gravitation halten die Planeten und andere Himmelskörper in einer Umlaufbahn um sie."
11 +}
customer_portal/src/lang/locales/en.json new
+11
@@ -0,0 +1,11 @@
1 +{
2 + "locales": {
3 + "it": "Italian",
4 + "en": "English",
5 + "jp": "Japanese",
6 + "fr": "French",
7 + "de": "German",
8 + "es": "Spanish"
9 + },
10 + "sun_description": "The sun is a medium-sized star located at the center of our solar system. It is a source of light and heat that sustains life on Earth. Composed mainly of hydrogen and helium, it generates energy through nuclear fusion. Its mass and gravity keep the planets and other celestial bodies in orbit around it."
11 +}
customer_portal/src/lang/locales/es.json new
+11
@@ -0,0 +1,11 @@
1 +{
2 + "locales": {
3 + "it": "Italiano",
4 + "en": "Inglés",
5 + "jp": "Japonés",
6 + "fr": "Francés",
7 + "de": "Alemán",
8 + "es": "Español"
9 + },
10 + "sun_description": "El sol es una estrella de tamaño medio ubicada en el centro de nuestro sistema solar. Es una fuente de luz y calor que sustenta la vida en la Tierra. Compuesto principalmente por hidrógeno y helio, genera energía a través de la fusión nuclear. Su masa y gravedad mantienen en órbita a los planetas y otros cuerpos celestes en el sistema solar."
11 +}
customer_portal/src/lang/locales/fr.json new
+11
@@ -0,0 +1,11 @@
1 +{
2 + "locales": {
3 + "it": "Italien",
4 + "en": "Anglais",
5 + "jp": "Japonais",
6 + "fr": "Français",
7 + "de": "Allemand",
8 + "es": "Espagnol"
9 + },
10 + "sun_description": "Le soleil est une étoile de taille moyenne située au centre de notre système solaire. Il est une source de lumière et de chaleur qui permet la vie sur Terre. Principalement composé d'hydrogène et d'hélium, il génère de l'énergie par fusion nucléaire. Sa masse et sa gravité maintiennent en orbite les planètes et autres corps célestes du système solaire."
11 +}
customer_portal/src/lang/locales/index.ts new
+6
@@ -0,0 +1,6 @@
1 +export { default as de } from "./de.json"
2 +export { default as en } from "./en.json"
3 +export { default as es } from "./es.json"
4 +export { default as fr } from "./fr.json"
5 +export { default as it } from "./it.json"
6 +export { default as jp } from "./jp.json"
customer_portal/src/lang/locales/it.json new
+11
@@ -0,0 +1,11 @@
1 +{
2 + "locales": {
3 + "it": "Italiano",
4 + "en": "Inglese",
5 + "jp": "Giapponese",
6 + "fr": "Francese",
7 + "de": "Tedesco",
8 + "es": "Spagnolo"
9 + },
10 + "sun_description": "Il sole è una stella di dimensioni medie che si trova al centro del nostro sistema solare. È una sorgente di luce e calore che permette la vita sulla Terra. Composto principalmente da idrogeno e elio, genera energia attraverso la fusione nucleare. La sua massa e la sua gravità mantengono in orbita i pianeti e gli altri corpi celesti nel sistema solare."
11 +}
customer_portal/src/lang/locales/jp.json new
+11
@@ -0,0 +1,11 @@
1 +{
2 + "locales": {
3 + "it": "イタリア語 (Italiago)",
4 + "en": "英語 (Eigo)",
5 + "jp": "日本語 (Nihongo)",
6 + "fr": "フランス語 (Furansugo)",
7 + "de": "ドイツ語 (Doitsugo)",
8 + "es": "スペイン語 (Supeingo)"
9 + },
10 + "sun_description": "太陽は、太陽系の中心に位置する中程度の大きさの星です。地球上の生命を維持する光と熱の源です。主に水素とヘリウムで構成され、核融合によってエネルギーを生成します。その質量と重力によって、惑星や他の天体は太陽の周りを軌道上に保ちます。"
11 +}
customer_portal/src/main.ts
+6 -2
@@ -1,4 +1,5 @@
1 import { createApp } from "vue"
2 +import i18n from "@/lang"
3 import { createPinia } from "pinia"
4 import { createPersistedState } from "pinia-plugin-persistedstate"
5 import App from "./App.vue"
@@ -7,13 +8,16 @@ import router from "./router"
8 // Import basic CSS
9 import "./styles/main.css"
10
10 -// Setup Pinia store
11 +const meta = document.createElement("meta")
12 +meta.name = "naive-ui-style"
13 +document.head.appendChild(meta)
14 +
15 const pinia = createPinia()
16 pinia.use(createPersistedState())
17
18 const app = createApp(App)
15 -
19 app.use(pinia)
20 +app.use(i18n)
21 app.use(router)
22
23 app.mount("#app")
customer_portal/src/router/index.ts
+1 -1
@@ -77,7 +77,7 @@ const routes = [
77 meta: { requiresAuth: true }
78 },
79 {
80 - path: "/dashboards/view/:id",
80 + path: "/dashboards/:id",
81 name: "DashboardView",
82 component: DashboardViewPage,
83 meta: { requiresAuth: true }
customer_portal/src/stores/i18n.ts new
+69
@@ -0,0 +1,69 @@
1 +import type { NDateLocale, NLocale } from "naive-ui"
2 +import type { WritableComputedRef } from "vue"
3 +import type { LocaleCodes } from "@/lang/config"
4 +import {
5 + dateDeDE,
6 + dateEnUS,
7 + dateEsAR,
8 + dateFrFR,
9 + dateItIT,
10 + dateJaJP,
11 + deDE,
12 + enUS,
13 + esAR,
14 + frFR,
15 + itIT,
16 + jaJP
17 +} from "naive-ui"
18 +import { acceptHMRUpdate, defineStore } from "pinia"
19 +import { nextTick } from "vue"
20 +import { useI18n } from "vue-i18n"
21 +import dayjs from "@/utils/dayjs"
22 +
23 +export const useLocalesStore = defineStore("i18n", {
24 + state: () => {
25 + nextTick(() => {
26 + const dayjsLocale = useLocalesStore().locale === "jp" ? "ja" : useLocalesStore().locale
27 + dayjs.locale(dayjsLocale)
28 + })
29 +
30 + return {
31 + locale: useI18n().locale as WritableComputedRef<LocaleCodes, string>,
32 + availableLocales: useI18n().availableLocales as LocaleCodes[]
33 + }
34 + },
35 + actions: {
36 + setLocale(locale: LocaleCodes): LocaleCodes {
37 + const dayjsLocale = locale === "jp" ? "ja" : locale
38 + dayjs.locale(dayjsLocale)
39 + this.locale = locale
40 + return locale
41 + }
42 + },
43 + getters: {
44 + naiveuiLocales(): { code: LocaleCodes; ui: NLocale; date: NDateLocale }[] {
45 + return [
46 + { code: "de", ui: deDE, date: dateDeDE },
47 + { code: "en", ui: enUS, date: dateEnUS },
48 + { code: "es", ui: esAR, date: dateEsAR },
49 + { code: "fr", ui: frFR, date: dateFrFR },
50 + { code: "it", ui: itIT, date: dateItIT },
51 + { code: "jp", ui: jaJP, date: dateJaJP }
52 + ]
53 + },
54 + naiveuiLocale(state): NLocale | undefined {
55 + return this.naiveuiLocales.find(locale => locale.code === state.locale)?.ui
56 + },
57 + naiveuiDateLocale(state): NDateLocale | undefined {
58 + return this.naiveuiLocales.find(locale => locale.code === state.locale)?.date
59 + }
60 + },
61 + persist: {
62 + // @ts-expect-error "Type instantiation is excessively deep and possibly infinite" ts(2589)
63 + pick: ["locale"]
64 + }
65 +})
66 +
67 +if (import.meta.hot) {
68 + import.meta.hot.accept(acceptHMRUpdate(useLocalesStore, import.meta.hot))
69 +}
customer_portal/src/stores/main.ts new
+25
@@ -0,0 +1,25 @@
1 +import type { LoadingBarInst } from "naive-ui/es/loading-bar/src/LoadingBarProvider"
2 +import { acceptHMRUpdate, defineStore } from "pinia"
3 +
4 +export const useMainStore = defineStore("main", {
5 + state: () => ({
6 + forceRefresh: Date.now(),
7 + loadingBar: null as LoadingBarInst | null
8 + }),
9 + actions: {
10 + softReload() {
11 + this.forceRefresh = Date.now()
12 + this.loadingBar?.start()
13 + setTimeout(() => {
14 + this.loadingBar?.finish()
15 + }, 1000)
16 + },
17 + setLoadingBar(loadingBar: LoadingBarInst) {
18 + this.loadingBar = loadingBar
19 + }
20 + }
21 +})
22 +
23 +if (import.meta.hot) {
24 + import.meta.hot.accept(acceptHMRUpdate(useMainStore, import.meta.hot))
25 +}
customer_portal/src/stores/settings.ts new
+56
@@ -0,0 +1,56 @@
1 +import { acceptHMRUpdate, defineStore } from "pinia"
2 +
3 +export const useSettingsStore = defineStore("settings", {
4 + state: () => ({
5 + settings: {
6 + dateFormat: "MM/DD/YYYY",
7 + hours24: true
8 + },
9 + dateFormats: ["MM/DD/YYYY", "DD/MM/YYYY"]
10 + }),
11 + actions: {
12 + setDateFormat(format: string) {
13 + this.settings.dateFormat = format
14 + },
15 + setHours24(hours24: boolean) {
16 + this.settings.hours24 = hours24
17 + }
18 + },
19 + getters: {
20 + dateFormatsAvailable(state) {
21 + return state.dateFormats
22 + },
23 + hours24(state) {
24 + return state.settings.hours24
25 + },
26 + rawDateFormat(state) {
27 + return state.settings.dateFormat
28 + },
29 + dateFormat(state) {
30 + const separator = " "
31 + const date = state.settings.dateFormat
32 + const time = state.settings.hours24 ? "HH:mm" : "h:mm a"
33 + const timesec = state.settings.hours24 ? "HH:mm:ss" : "h:mm:ss a"
34 + const timesecmill = "HH:mm:ss.SSS"
35 +
36 + return {
37 + date: `${date}`,
38 + datetime: `${date}${separator}${time}`,
39 + datetimesec: `${date}${separator}${timesec}`,
40 + datetimesecmill: `${date}${separator}${timesecmill}`,
41 + /** "HH:mm" or "h:mm a" */
42 + time,
43 + /** "HH:mm:ss" or "h:mm:ss a" */
44 + timesec,
45 + timesecmill
46 + }
47 + }
48 + },
49 + persist: {
50 + pick: ["settings"]
51 + }
52 +})
53 +
54 +if (import.meta.hot) {
55 + import.meta.hot.accept(acceptHMRUpdate(useSettingsStore, import.meta.hot))
56 +}
customer_portal/src/stores/theme.ts new
+197
@@ -0,0 +1,197 @@
1 +import type { GlobalThemeOverrides, ThemeCommonVars } from "naive-ui"
2 +import type { BuiltInGlobalTheme } from "naive-ui/es/themes/interface"
3 +import type { Layout, RouterTransition } from "@/types/theme.d"
4 +import { useWindowSize } from "@vueuse/core"
5 +import _get from "lodash/get"
6 +import _set from "lodash/set"
7 +import { darkTheme, lightTheme } from "naive-ui"
8 +import { acceptHMRUpdate, defineStore } from "pinia"
9 +import { watch } from "vue"
10 +import { getCssVars, getDefaultState, getThemeOverrides } from "@/theme"
11 +import { ThemeNameEnum } from "@/types/theme.d"
12 +
13 +export const useThemeStore = defineStore("theme", {
14 + state: () => getDefaultState(),
15 + actions: {
16 + setLayout(layout: Layout): void {
17 + this.layout = layout
18 + },
19 + setRTL(rtl: boolean): void {
20 + this.rtl = rtl
21 + },
22 + setBoxed(boxed: boolean): void {
23 + this.boxed.enabled = boxed
24 + },
25 + setFooterShow(show: boolean): void {
26 + this.footer.show = show
27 + },
28 + setToolbarBoxed(boxed: boolean): void {
29 + this.boxed.toolbar = boxed
30 + },
31 + setRouterTransition(routerTransition: RouterTransition): void {
32 + this.routerTransition = routerTransition
33 + },
34 + setTheme(themeName: ThemeNameEnum): void {
35 + this.themeName = themeName
36 + },
37 + setThemeLight(): void {
38 + this.themeName = ThemeNameEnum.Light
39 + },
40 + setThemeDark(): void {
41 + this.themeName = ThemeNameEnum.Dark
42 + },
43 + setColor(theme: ThemeNameEnum, colorName: string, color: string): void {
44 + ;(this.colors[theme] as Record<string, string>)[colorName] = color
45 + },
46 + toggleTheme(): void {
47 + if (this.isThemeDark) {
48 + this.setThemeLight()
49 + } else {
50 + this.setThemeDark()
51 + }
52 + },
53 + toggleSidebar(): void {
54 + this.sidebar.collapsed = !this.sidebar.collapsed
55 + },
56 + refreshSidebar(): void {
57 + // this is useful in context like NUXT
58 + this.sidebar.collapsed = !this.sidebar.collapsed
59 + setTimeout(() => {
60 + this.sidebar.collapsed = !this.sidebar.collapsed
61 + }, 10)
62 + },
63 + openSidebar(): void {
64 + this.sidebar.collapsed = false
65 + },
66 + closeSidebar(): void {
67 + this.sidebar.collapsed = true
68 + },
69 + updateResponsiveVars() {
70 + for (const key in this.responsive.override) {
71 + if (_get(this, key) && key in this.responsive.override) {
72 + _set(
73 + this,
74 + key,
75 + window.innerWidth <= this.responsive.breakpoint
76 + ? this.responsive.override[key as keyof typeof this.responsive.override].mobile
77 + : this.responsive.override[key as keyof typeof this.responsive.override].desk
78 + )
79 + }
80 + }
81 + },
82 + ensureSidebarState() {
83 + // auto close sidebar on resize
84 + if (this.sidebar.autoClose) {
85 + if (!this.sidebar.collapsed && window.innerWidth <= this.sidebar.autoCloseBreakpoint) {
86 + this.sidebar.collapsed = true
87 + }
88 + }
89 + },
90 + setDocumentThemeName(val: ThemeNameEnum, old?: ThemeNameEnum) {
91 + if (document) {
92 + const html = document.children[0] as HTMLElement
93 + if (old) {
94 + html.classList.remove(`theme-${old}`)
95 + }
96 + html.classList.add(`theme-${val}`)
97 + }
98 + },
99 + // This function allows you to utilize the values in the style object as variables within your CSS/SCSS code like: var(--bg-default-color)
100 + setCssGlobalVars() {
101 + if (document) {
102 + const html = document.children[0] as HTMLElement
103 + const body = document.getElementsByTagName("body")?.[0]
104 + if (body) {
105 + if (this.isRTL) {
106 + body.classList.add("direction-rtl")
107 + body.classList.remove("direction-ltr")
108 + } else {
109 + body.classList.remove("direction-rtl")
110 + body.classList.add("direction-ltr")
111 + }
112 + }
113 + // html.dir = this.isRTL ? "rtl" : "ltr"
114 + const { style: htmlStyle } = html
115 + for (const key in this.style) {
116 + const value = this.style[key]
117 +
118 + if (!value) continue
119 +
120 + htmlStyle.setProperty(`--${key}`, value)
121 + }
122 + }
123 + },
124 + startWatchers() {
125 + const { width } = useWindowSize()
126 +
127 + watch([() => this.isRTL, () => this.style], () => {
128 + this.setCssGlobalVars()
129 + })
130 +
131 + watch(
132 + () => this.themeName,
133 + (val, old) => {
134 + this.setDocumentThemeName(val, old)
135 + }
136 + )
137 +
138 + watch(width, () => {
139 + this.updateResponsiveVars()
140 + this.ensureSidebarState()
141 + })
142 + },
143 + initTheme() {
144 + this.updateResponsiveVars()
145 + this.ensureSidebarState()
146 + this.setCssGlobalVars()
147 + this.setDocumentThemeName(this.themeName)
148 + this.startWatchers()
149 + }
150 + },
151 + getters: {
152 + naiveTheme(state): BuiltInGlobalTheme {
153 + return state.themeName === ThemeNameEnum.Dark ? darkTheme : lightTheme
154 + },
155 + themeOverrides(state): GlobalThemeOverrides {
156 + return getThemeOverrides(state)
157 + },
158 + darkPrimaryColor(state): string {
159 + return state.colors.dark.primary
160 + },
161 + lightPrimaryColor(state): string {
162 + return state.colors.light.primary
163 + },
164 + naiveCommon(): ThemeCommonVars {
165 + return { ...this.naiveTheme.common, ...this.themeOverrides.common }
166 + },
167 + style(state): { [key: string]: string } {
168 + return getCssVars(state, this)
169 + },
170 + isThemeDark(state): boolean {
171 + return state.themeName === ThemeNameEnum.Dark
172 + },
173 + isThemeLight(state): boolean {
174 + return state.themeName === ThemeNameEnum.Light
175 + },
176 + isBoxed(state): boolean {
177 + return state.boxed.enabled
178 + },
179 + isRTL(state): boolean {
180 + return state.rtl
181 + },
182 + isFooterShown(state): boolean {
183 + return state.footer.show
184 + },
185 + isToolbarBoxed(state): boolean {
186 + return state.boxed.toolbar && state.boxed.enabled
187 + }
188 + },
189 + persist: {
190 + // use this param to save specific state chunk on localStorage
191 + pick: ["layout", "themeName", "routerTransition", "boxed", "sidebar.collapsed"]
192 + }
193 +})
194 +
195 +if (import.meta.hot) {
196 + import.meta.hot.accept(acceptHMRUpdate(useThemeStore, import.meta.hot))
197 +}
customer_portal/src/theme/index.ts new
+220
@@ -0,0 +1,220 @@
1 +import type { GlobalThemeOverrides, ThemeCommonVars } from "naive-ui"
2 +import { useOsTheme } from "naive-ui"
3 +import tokens from "@/design-tokens.json"
4 +import { Layout, RouterTransition, ThemeNameEnum } from "@/types/theme.d"
5 +import { colorToArray, expandPattern, getThemeColors, getTypeValue } from "@/utils/theme"
6 +
7 +type ThemeState = ReturnType<typeof getDefaultState>
8 +
9 +interface ThemeGetters {
10 + naiveCommon: ThemeCommonVars
11 +}
12 +
13 +const osTheme = useOsTheme()
14 +
15 +export function getDefaultState() {
16 + return {
17 + layout: Layout.VerticalNav, // Type of layout, with vertical or horizontal navigation
18 + themeName: /*osTheme.value === "dark" ? ThemeNameEnum.Dark :*/ ThemeNameEnum.Light, // Theme name (Dark, Light), with fallback to the light theme
19 + routerTransition: RouterTransition.FadeUp, // Type of transition for the router
20 + routerTransitionDuration: 0.3, // Duration of the router transition in seconds
21 + rtl: false, // RTL (right to left) mode toggle
22 + boxed: {
23 + enabled: true, // Choose whether to apply a maximum width to the page
24 + toolbar: true, // Choose whether to apply the maximum width to the toolbar as well
25 + width: 1600 // Maximum width size in pixels
26 + },
27 + sidebar: {
28 + autoClose: true, // Choose whether to automatically close the sidebar when the view goes below the "autoCloseBreakpoint" value
29 + collapsed: false, // Indicates if the sidebar is collapsed
30 + autoCloseBreakpoint: 1000, // Breakpoint for the automatic closing of the sidebar (in pixels)
31 + animEase: "ease-in-out", // Type of easing for animations
32 + animDuration: 0.3, // Duration of sidebar animations (in seconds)
33 + openWidth: 300, // Width of the open sidebar (in pixels)
34 + closeWidth: 64 // Width of the closed sidebar (in pixels)
35 + },
36 + footer: {
37 + show: true // Show or hide the footer
38 + },
39 + responsive: {
40 + breakpoint: 700, // Breakpoint in pixels (Desk -> Mobile)
41 + // Parameters to be adjusted based on the breakpoint
42 + override: {
43 + viewPadding: {
44 + desk: 40, // View padding for desktop
45 + mobile: 20 // View padding for mobile devices
46 + },
47 + toolbarHeight: {
48 + desk: 80, // Height of the toolbar for desktop
49 + mobile: 70 // Height of the toolbar for mobile devices
50 + }
51 + }
52 + },
53 + toolbarHeight: 80, // Default toolbar height (in pixels)
54 + viewPadding: 40, // Default view padding (in pixels)
55 + headerBarHeight: 60, // Height of the header bar (in pixels)
56 + colors: tokens.colors, // Color definitions from the token
57 + borderRadius: tokens.borderRadius, // Border radius from the token
58 + lineHeight: tokens.lineHeight, // Line height from the token
59 + fontSize: tokens.fontSize, // Font size from the token
60 + fontWeight: tokens.fontWeight, // Font weight from the token
61 + fontFamily: tokens.fontFamily, // Font family from the token
62 + typography: tokens.typography // Typography configurations from the token
63 + }
64 +}
65 +
66 +export function getThemeOverrides(state: ThemeState): GlobalThemeOverrides {
67 + const {
68 + primary,
69 + success,
70 + warning,
71 + error,
72 + info,
73 + background,
74 + backgroundSecondary,
75 + bodyBackground,
76 + text,
77 + textTertiary,
78 + border,
79 + hover
80 + } = state.colors[state.themeName]
81 +
82 + const themeColors = getThemeColors({ primary, success, warning, error, info })
83 +
84 + const lineHeight = state.lineHeight.default
85 + const borderRadius = state.borderRadius.default
86 + const borderRadiusSmall = state.borderRadius.small
87 + const borderColor = border
88 + const hoverColor = hover
89 +
90 + return {
91 + common: {
92 + ...themeColors,
93 + textColorBase: text,
94 + textColor1: text,
95 + textColor2: text,
96 + textColor3: textTertiary,
97 + bodyColor: bodyBackground,
98 + baseColor: background,
99 + popoverColor: background,
100 + cardColor: background,
101 + modalColor: background,
102 + lineHeight,
103 + borderRadius,
104 + borderRadiusSmall,
105 + fontSize: state.fontSize.default,
106 + fontWeight: state.fontWeight.default,
107 + fontWeightStrong: state.fontWeight.strong,
108 + fontFamily: state.fontFamily.default,
109 + fontFamilyMono: state.fontFamily.mono,
110 + borderColor,
111 + hoverColor,
112 + dividerColor: borderColor
113 + },
114 + Card: {
115 + color: background,
116 + colorEmbedded: backgroundSecondary,
117 + titleFontSizeSmall: state.fontSize.cardTitle,
118 + titleFontSizeMedium: state.fontSize.cardTitle,
119 + titleFontSizeLarge: state.fontSize.cardTitle,
120 + titleFontSizeHuge: state.fontSize.cardTitle,
121 + titleFontWeight: state.fontWeight.cardTitle
122 + },
123 + LoadingBar: {
124 + colorLoading: primary
125 + },
126 + Tag: {
127 + colorBordered: "rgba(0, 0, 0, 0.1)"
128 + },
129 + Typography: {
130 + headerFontSize1: getTypeValue(state, state.typography.h1.fontSize),
131 + headerFontSize2: getTypeValue(state, state.typography.h2.fontSize),
132 + headerFontSize3: getTypeValue(state, state.typography.h3.fontSize),
133 + headerFontSize4: getTypeValue(state, state.typography.h4.fontSize),
134 + headerFontSize5: getTypeValue(state, state.typography.h5.fontSize),
135 + headerFontSize6: getTypeValue(state, state.typography.h6.fontSize)
136 + }
137 + }
138 +}
139 +
140 +export function getCssVars(state: ThemeState, getters: ThemeGetters): { [key: string]: string } {
141 + const naive = getters.naiveCommon
142 +
143 + const bgColor = naive.baseColor
144 + const bgSecondaryColor = state.colors[state.themeName].backgroundSecondary
145 + const fgColor = state.colors[state.themeName].text
146 + const fgSecondaryColor = state.colors[state.themeName].textSecondary
147 + const fgTertiaryColor = state.colors[state.themeName].textTertiary
148 +
149 + const bgSidebar = state.colors[state.themeName].sidebarBackground
150 + const bgBody = state.colors[state.themeName].bodyBackground
151 +
152 + const boxedWidth = state.boxed.width
153 + const routerTransitionDuration = state.routerTransitionDuration
154 + const sidebarAnimEase = state.sidebar.animEase
155 + const sidebarAnimDuration = state.sidebar.animDuration
156 + const sidebarOpenWidth = state.sidebar.openWidth
157 + const sidebarCloseWidth = state.sidebar.closeWidth
158 + const toolbarHeight = state.toolbarHeight
159 + const viewPadding = state.viewPadding
160 + const headerBarHeight = state.headerBarHeight
161 + const fontFamily = state.fontFamily.default
162 + const fontFamilyDisplay = state.fontFamily.display
163 + const fontFamilyMono = state.fontFamily.mono
164 + const fontSize = state.fontSize.default
165 + const lineHeight = state.lineHeight.default
166 +
167 + const borderRadius = state.borderRadius.default
168 + const borderRadiusSmall = state.borderRadius.small
169 +
170 + const bezierEase = naive.cubicBezierEaseInOut
171 +
172 + // This style object, imported via the themeStore, will be available application-wide and is exposed in the HTML tag as a list of CSS variables, which you can use in your CSS/SCSS code like: var(--bg-default-color)
173 + const styleObject: Record<string, string> = {
174 + "bg-body-color": `${bgBody}`,
175 + "bg-sidebar-color": `${bgSidebar}`,
176 +
177 + "fg-default-color": `${fgColor}`,
178 + "fg-secondary-color": `${fgSecondaryColor}`,
179 + "fg-tertiary-color": `${fgTertiaryColor}`,
180 + "bg-default-color": `${bgColor}`,
181 + "bg-secondary-color": `${bgSecondaryColor}`,
182 +
183 + "bezier-ease": `${bezierEase}`,
184 + "router-transition-duration": `${routerTransitionDuration}s`,
185 + "sidebar-anim-ease": `${sidebarAnimEase}`,
186 + "sidebar-anim-duration": `${sidebarAnimDuration}s`,
187 + "sidebar-open-width": `${sidebarOpenWidth}px`,
188 + "sidebar-close-width": `${sidebarCloseWidth}px`,
189 + "boxed-width": `${boxedWidth}px`,
190 + "toolbar-height": `${toolbarHeight}px`,
191 + "header-bar-height": `${headerBarHeight}px`,
192 + "view-padding": `${viewPadding}px`,
193 + "border-radius": `${borderRadius}`,
194 + "border-radius-small": `${borderRadiusSmall}`,
195 +
196 + "font-family": `${fontFamily}`,
197 + "font-family-display": `${fontFamilyDisplay}`,
198 + "font-family-mono": `${fontFamilyMono}`,
199 + "font-size": `${fontSize}`,
200 + "line-height": `${lineHeight}`
201 + }
202 +
203 + // import colors by patterns
204 + for (const pattern of ["extra(1|2|3|4)", "primary", "success", "warning", "error", "info", "border", "hover"]) {
205 + const keys = expandPattern(pattern)
206 +
207 + for (const key of keys) {
208 + styleObject[`${key}-color`] = (state.colors[state.themeName] as Record<string, string>)[key] ?? ""
209 + }
210 + }
211 +
212 + // add RGB values variant
213 + for (const [key, value] of Object.entries(styleObject)) {
214 + if (key.endsWith("-color")) {
215 + styleObject[`${key}-rgb`] = colorToArray(value, "rgb").join(" ")
216 + }
217 + }
218 +
219 + return styleObject
220 +}
customer_portal/src/types/common.d.ts new
+21
@@ -0,0 +1,21 @@
1 +import type { AxiosError, AxiosResponse } from "axios"
2 +import type { FlaskBaseResponse } from "./flask"
3 +
4 +export type OsTypesFull = "Unknown" | "Windows" | "MacOS" | "UNIX" | "Linux"
5 +export type OsTypesLower = "linux" | "windows" | "macos"
6 +export type SafeAny = string | number | boolean | object | Date | null
7 +export type ApiError = AxiosError<FlaskBaseResponse>
8 +export type ApiCommonResponse<T = unknown> = AxiosResponse<FlaskBaseResponse & T>
9 +export type DeepNullable<T> = {
10 + [K in keyof T]: DeepNullable<T[K]> | null
11 +}
12 +
13 +export type Nullable<T> = {
14 + [K in keyof T]: T[K] | null
15 +}
16 +
17 +export type RecursiveKeyOf<TObj extends object> = {
18 + [TKey in keyof TObj & (string | number)]: TObj[TKey] extends object
19 + ? `${TKey}` | `${TKey}.${RecursiveKeyOf<TObj[TKey]>}`
20 + : `${TKey}`
21 +}[keyof TObj & (string | number)]
customer_portal/src/types/dashboards.d.ts new
+72
@@ -0,0 +1,72 @@
1 +export type DashboardPanelType = "stat" | "pie" | "bar_h" | "histogram"
2 +
3 +export interface DashboardPanel {
4 + id: string
5 + title: string
6 + type: DashboardPanelType
7 + w: number
8 + h: number
9 + lucene: string
10 + field?: string
11 + size?: number
12 +}
13 +
14 +export interface DashboardTemplate {
15 + id: string
16 + title: string
17 + description: string
18 + panels: DashboardPanel[]
19 +}
20 +
21 +export interface DashboardCategory {
22 + id: string
23 + title: string
24 + description: string
25 + vendor: string
26 + product: string
27 + event_type: string
28 + tags: string[]
29 + color: string
30 + icon: string
31 +}
32 +
33 +export interface DashboardCategoryWithTemplates extends DashboardCategory {
34 + templates: DashboardTemplate[]
35 +}
36 +
37 +export interface EnabledDashboard {
38 + id: number
39 + customer_code: string
40 + event_source_id: number
41 + library_card: string
42 + template_id: string
43 + display_name: string
44 + created_at: string
45 +}
46 +
47 +export interface EnableDashboardPayload {
48 + customer_code: string
49 + event_source_id: number
50 + library_card: string
51 + template_id: string
52 + display_name: string
53 +}
54 +
55 +export interface PanelResult {
56 + type: string
57 + value: number | null
58 + labels: string[]
59 + data: number[]
60 + error: string | null
61 +}
62 +
63 +export interface PanelDataResponse {
64 + panels: Record<string, PanelResult>
65 + template: DashboardTemplate
66 + dashboard_id: number
67 + customer_code: string
68 + source_name: string
69 + accent_color: string
70 + success: boolean
71 + message: string
72 +}
customer_portal/src/types/theme.d.ts new
+27
@@ -0,0 +1,27 @@
1 +export enum Layout {
2 + VerticalNav = "VerticalNav",
3 + Blank = "Blank"
4 +}
5 +
6 +export enum RouterTransition {
7 + Fade = "fade",
8 + FadeUp = "fade-up",
9 + FadeBottom = "fade-bottom",
10 + FadeLeft = "fade-left",
11 + FadeRight = "fade-right"
12 +}
13 +
14 +export enum ThemeNameEnum {
15 + Dark = "dark",
16 + Light = "light"
17 +}
18 +
19 +export type ColorScene = "" | "Suppl" | "Hover" | "Pressed"
20 +export type ColorType = "primary" | "success" | "warning" | "error" | "info"
21 +export type ColorKey = `${ColorType}Color${ColorScene}`
22 +export type ThemeColor = Partial<Record<ColorKey, string>>
23 +
24 +export interface ColorAction {
25 + scene: ColorScene
26 + handler: (color: string) => string
27 +}
customer_portal/src/utils/dayjs.ts new
+25
@@ -0,0 +1,25 @@
1 +import dayjs from "dayjs"
2 +import customParseFormat from "dayjs/plugin/customParseFormat"
3 +import duration from "dayjs/plugin/duration"
4 +import relativeTime from "dayjs/plugin/relativeTime"
5 +import timezone from "dayjs/plugin/timezone"
6 +import utc from "dayjs/plugin/utc"
7 +import "dayjs/locale/it"
8 +import "dayjs/locale/en"
9 +import "dayjs/locale/de"
10 +import "dayjs/locale/es"
11 +import "dayjs/locale/fr"
12 +import "dayjs/locale/ja"
13 +
14 +/*
15 +import isSameOrAfter from "dayjs/plugin/isSameOrAfter"
16 +dayjs.extend(isSameOrAfter)
17 +*/
18 +dayjs.extend(utc)
19 +dayjs.extend(relativeTime)
20 +dayjs.extend(duration)
21 +dayjs.extend(customParseFormat)
22 +dayjs.extend(timezone)
23 +dayjs.tz.setDefault(dayjs.tz.guess())
24 +
25 +export default dayjs
customer_portal/src/utils/format.ts new
+18
@@ -0,0 +1,18 @@
1 +import bytes from "bytes"
2 +import _toNumber from "lodash/toNumber"
3 +import dayjs from "@/utils/dayjs"
4 +
5 +export function formatBytes(val: string | number) {
6 + return bytes(_toNumber(val))
7 +}
8 +
9 +export function formatDate(date: Date | string | number, format: string) {
10 + let parsedDate = date
11 + if (typeof date === "number" && date.toString().length >= 15) {
12 + parsedDate = date / 1000
13 + }
14 + const datejs = dayjs(parsedDate)
15 + if (!datejs.isValid()) return date
16 +
17 + return datejs.format(format)
18 +}
customer_portal/src/utils/highlighter.ts new
+77
@@ -0,0 +1,77 @@
1 +import type { BundledLanguage, BundledTheme, HighlighterGeneric } from "shiki"
2 +import { createHighlighter } from "shiki"
3 +import { createOnigurumaEngine } from "shiki/engine/oniguruma"
4 +
5 +const THEME_LIGHT = "slack-ochin"
6 +const THEME_DARK = "aurora-x"
7 +
8 +let highlighterInstance: HighlighterGeneric<BundledLanguage, BundledTheme> | null = null
9 +let highlighterPromise: Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> | null = null
10 +
11 +export async function getHighlighter() {
12 + if (highlighterInstance) {
13 + return highlighterInstance
14 + }
15 + if (highlighterPromise) {
16 + return highlighterPromise
17 + }
18 +
19 + highlighterPromise = createHighlighter({
20 + themes: [import("shiki/themes/slack-ochin.mjs"), import("shiki/themes/aurora-x.mjs")],
21 + langs: [
22 + import("shiki/langs/javascript.mjs"),
23 + import("shiki/langs/typescript.mjs"),
24 + import("shiki/langs/powershell.mjs"),
25 + import("shiki/langs/shellscript.mjs"),
26 + import("shiki/langs/json.mjs"),
27 + import("shiki/langs/xml.mjs"),
28 + import("shiki/langs/yaml.mjs"),
29 + import("shiki/langs/html.mjs"),
30 + import("shiki/langs/scss.mjs"),
31 + import("shiki/langs/css.mjs"),
32 + import("shiki/langs/csharp.mjs"),
33 + import("shiki/langs/http.mjs"),
34 + import("shiki/langs/sql.mjs"),
35 + import("shiki/langs/lua.mjs"),
36 + import("shiki/langs/vb.mjs"),
37 + import("shiki/langs/php.mjs")
38 + ],
39 + engine: createOnigurumaEngine(() => import("shiki/wasm"))
40 + }).then(instance => {
41 + highlighterInstance = instance
42 + return instance
43 + })
44 +
45 + return highlighterPromise
46 +}
47 +
48 +export const codeThemes = {
49 + light: THEME_LIGHT,
50 + dark: THEME_DARK
51 +}
52 +
53 +const LEADING_WHITESPACE_REGEX = /^\s+/
54 +const TAB_CHAR_REGEX = /\t/g
55 +
56 +export function resetIndent(el: HTMLElement) {
57 + if (el) {
58 + let lines: string[] = el.innerHTML?.split("\n")
59 +
60 + if (lines?.length) {
61 + if (lines[0] === "") {
62 + lines.shift()
63 + }
64 +
65 + const matches = LEADING_WHITESPACE_REGEX.exec(lines[0] ?? "") ?? null
66 + const indentation = matches !== null ? (matches[0] ?? "") : ""
67 + if (indentation) {
68 + lines = lines.map(line => {
69 + line = line.replace(indentation, "")
70 + return line.replace(TAB_CHAR_REGEX, " ")
71 + })
72 +
73 + el.innerHTML = lines.join("\n").trim()
74 + }
75 + }
76 + }
77 +}
customer_portal/src/utils/index.ts new
+182
@@ -0,0 +1,182 @@
1 +import type { Component } from "vue"
2 +import type { OsTypesFull, SafeAny } from "@/types/common.d"
3 +import process from "node:process"
4 +import { isMobile as detectMobile } from "detect-touch-device"
5 +import { md5 } from "js-md5"
6 +import isDateObject from "lodash/isDate"
7 +import _trim from "lodash/trim"
8 +import { h } from "vue"
9 +import Icon from "@/components/common/Icon.vue"
10 +import dayjs from "./dayjs"
11 +
12 +// Transform File Instance in base64 string
13 +export function file2Base64(blob: Blob): Promise<string> {
14 + return new Promise((resolve, reject) => {
15 + const reader = new FileReader()
16 + reader.readAsDataURL(blob)
17 + reader.onload = () => resolve(reader.result as string)
18 + reader.onerror = error => reject(error)
19 + })
20 +}
21 +
22 +export function isEnvDev() {
23 + return process.env.NODE_ENV === "development"
24 +}
25 +export function isEnvTest() {
26 + return process.env.NODE_ENV === "test"
27 +}
28 +export function isEnvProd() {
29 + return process.env.NODE_ENV === "production"
30 +}
31 +
32 +export function isMobile() {
33 + return detectMobile
34 +}
35 +
36 +const URL_PATTERN = /^https?:\/\//i
37 +
38 +export function isUrlLike(text: string) {
39 + return URL_PATTERN.test(text)
40 +}
41 +
42 +export function renderIcon(icon: Component | string) {
43 + if (typeof icon === "string") {
44 + return () => h(Icon, { name: icon })
45 + } else {
46 + return () => h(Icon, null, { default: () => h(icon) })
47 + }
48 +}
49 +
50 +export function iconFromOs(os: string): string {
51 + switch (getOS(os).toLowerCase()) {
52 + case "windows":
53 + return "mdi:microsoft"
54 + case "macos":
55 + return "mdi:apple"
56 + case "linux":
57 + case "unix":
58 + return "mdi:linux"
59 + default:
60 + return "mdi:help-box"
61 + }
62 +}
63 +
64 +export function getOS(os: string): OsTypesFull {
65 + const test = os.toLowerCase()
66 + if (test.includes("mac") || test.includes("darwin") || test.includes("apple")) {
67 + return "MacOS"
68 + }
69 + if (test.includes("win") || test.includes("microsoft")) {
70 + return "Windows"
71 + }
72 + if (
73 + test.includes("linux") ||
74 + test.includes("ubuntu") ||
75 + test.includes("unix") ||
76 + test.includes("x11") ||
77 + test.includes("debian") ||
78 + test.includes("centos")
79 + ) {
80 + return "Linux"
81 + }
82 +
83 + return "Unknown"
84 +}
85 +
86 +export function getNavigatorOS(): OsTypesFull {
87 + let os: OsTypesFull = "Unknown"
88 + if (navigator.userAgent.includes("Win")) os = "Windows"
89 + if (navigator.userAgent.includes("Mac")) os = "MacOS"
90 + if (navigator.userAgent.includes("X11")) os = "UNIX"
91 + if (navigator.userAgent.includes("Linux")) os = "Linux"
92 +
93 + return os
94 +}
95 +
96 +export function delay(t: number) {
97 + return new Promise(res => setTimeout(res, t))
98 +}
99 +
100 +export function hashMD5(text: number | string) {
101 + return md5(text.toString())
102 +}
103 +
104 +export function price(
105 + amount: number,
106 + options: { currency?: "USD" | "EUR"; splitDecimal?: boolean } = { currency: "USD", splitDecimal: true }
107 +) {
108 + let symbol = ""
109 + switch (options.currency) {
110 + case "USD":
111 + symbol = "$"
112 + break
113 + case "EUR":
114 + symbol = "€"
115 + break
116 + }
117 +
118 + const price = options.splitDecimal ? (amount / 100).toFixed(2) : amount
119 +
120 + return `${symbol}${price}`
121 +}
122 +
123 +export function getBaseUrl() {
124 + return _trim(import.meta.env.VITE_API_URL, "/")
125 +}
126 +
127 +export function getNameInitials(name: string, cap?: number) {
128 + let initials = name.slice(0, 2)
129 +
130 + if (name.includes(" ")) {
131 + initials = name
132 + .split(" ")
133 + .map(chunk => chunk[0])
134 + .join()
135 + }
136 +
137 + return (cap ? initials.slice(0, cap) : initials).toUpperCase()
138 +}
139 +
140 +export function getAvatar(params: { seed: string; text?: string; size?: number; format?: "png" | "svg" }) {
141 + const format: "png" | "svg" = params.text ? "svg" : params.format || "svg"
142 +
143 + return `https://avatar.vercel.sh/${params.seed}.${format}?text=${params.text || ""}&size=${params.size || 32}`
144 +}
145 +
146 +const NUMERIC_TIMESTAMP_REGEX = /^\d{10,}$/
147 +
148 +export function isDate(val?: SafeAny): boolean {
149 + if (val === undefined || val === null || val === "") return false
150 +
151 + if (isDateObject(val)) return true
152 +
153 + const strVal = String(val)
154 +
155 + // Check for numeric timestamps (seconds, ms or µs)
156 + // We enforce a minimum length of 10 digits to avoid false positives like "188" or "2"
157 + if (NUMERIC_TIMESTAMP_REGEX.test(strVal)) {
158 + const num = Number.parseInt(strVal)
159 + // Handle ms (13 digits) or µs (16+ digits) by normalizing to ms
160 + const date = strVal.length >= 13 ? dayjs(num / 10 ** (strVal.length - 13)) : dayjs(num * 1000)
161 + return date.isValid()
162 + }
163 +
164 + // For ISO strings and other complex formats, we use a hybrid approach
165 + // 1. Check for ISO-like strings (containing T and possibly Z or +/- offset)
166 + if (strVal.includes("T")) {
167 + // dayjs() parser is quite robust for ISO 8601 even without explicit format
168 + return dayjs(strVal).isValid()
169 + }
170 +
171 + // 2. Strict parsing for regional formats
172 + const regionalFormats = ["DD/MM/YYYY", "MM/DD/YYYY", "DD-MM-YYYY", "MM-DD-YYYY", "YYYY-MM-DD", "YYYY-DD-MM"]
173 +
174 + return dayjs(strVal, regionalFormats, true).isValid()
175 +}
176 +
177 +export function formatCompactNumber(value: number | null | undefined): string {
178 + if (value == null) return "—"
179 + if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
180 + if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`
181 + return value.toLocaleString()
182 +}
customer_portal/src/utils/secure-storage.ts new
+60
@@ -0,0 +1,60 @@
1 +import SecureLS from "secure-ls"
2 +
3 +const secureLS = new SecureLS({
4 + encodingType: "aes",
5 + isCompression: false
6 +})
7 +
8 +const PREFIX_PERSISTED = "__persisted__"
9 +const PREFIX_SESSION = "__persisted-session__"
10 +
11 +export function persistentKey(options?: { session?: boolean }) {
12 + const prefix = options?.session ? PREFIX_SESSION : PREFIX_PERSISTED
13 + return (id: string) => `${prefix}${id}`
14 +}
15 +
16 +export function removePersistentSessionKey() {
17 + for (const key of secureLS.getAllKeys()) {
18 + if (key.includes(PREFIX_SESSION)) {
19 + secureLS.remove(key)
20 + }
21 + }
22 +}
23 +
24 +export function secureLocalStorage(options?: { session?: boolean }) {
25 + return {
26 + getItem(key: string) {
27 + try {
28 + return secureLS.get(persistentKey({ session: options?.session })(key))
29 + } catch (err) {
30 + console.warn(`[secureLocalStorage] Failed to get "${key}"`, err)
31 + return null
32 + }
33 + },
34 +
35 + setItem(key: string, value: string): void {
36 + try {
37 + secureLS.set(persistentKey({ session: options?.session })(key), value)
38 + } catch (err) {
39 + console.warn(`[secureLocalStorage] Failed to set "${key}"`, err)
40 + }
41 + },
42 +
43 + removeItem(key: string): void {
44 + try {
45 + secureLS.remove(persistentKey({ session: options?.session })(key))
46 + } catch (err) {
47 + console.warn(`[secureLocalStorage] Failed to remove "${key}"`, err)
48 + }
49 + }
50 + }
51 +}
52 +
53 +export function piniaStorage(options?: { session?: boolean }) {
54 + return {
55 + getItem: (key: string) => secureLS.get(persistentKey({ session: options?.session })(key)),
56 + setItem: (key: string, value: unknown) => secureLS.set(persistentKey({ session: options?.session })(key), value)
57 + }
58 +}
59 +
60 +export { secureLS }
customer_portal/src/utils/theme.ts new
+110
@@ -0,0 +1,110 @@
1 +import type { ColorAction, ColorKey, ColorType, ThemeColor } from "@/types/theme"
2 +import { colord } from "colord"
3 +import _get from "lodash/get"
4 +
5 +export const COLOR_SHADES = ["005", "010", "015", "020", "030", "040", "050", "060", "070", "080", "090"] as const
6 +
7 +export type ColorShade = (typeof COLOR_SHADES)[number]
8 +
9 +export function toggleSidebarClass(
10 + sidebarCollapsed: boolean,
11 + elementId: string,
12 + classOpen: string,
13 + classClose: string
14 +) {
15 + const el = window?.document?.getElementById(elementId)
16 + if (!el) return
17 +
18 + el.classList.toggle(classOpen, !sidebarCollapsed)
19 + el.classList.toggle(classClose, sidebarCollapsed)
20 +}
21 +
22 +export function colorToArray(color: string, output: "rgb" | "hsl"): number[] {
23 + const colorObject = colord(color)
24 +
25 + switch (output) {
26 + case "rgb": {
27 + const { r, g, b } = colorObject.toRgb()
28 + return [r, g, b]
29 + }
30 + case "hsl": {
31 + const { h, s, l } = colorObject.toHsl()
32 + return [h, s, l]
33 + }
34 + default:
35 + throw new Error("Invalid output type")
36 + }
37 +}
38 +
39 +export function exposure(color: string, amount: number): string {
40 + return colord(color)
41 + .lighten(amount) /* .desaturate(Math.abs(amount)) */
42 + .toHex()
43 +}
44 +
45 +export function getColorAlphaShades(color: string): { [key in ColorShade]: string } {
46 + return COLOR_SHADES.reduce<{ [key in ColorShade]: string }>(
47 + (acc, shade) => {
48 + acc[shade] = colord(color)
49 + .alpha(Number.parseInt(shade, 10) / 100)
50 + .toRgbString()
51 + return acc
52 + },
53 + {} as { [key in ColorShade]: string }
54 + )
55 +}
56 +
57 +export function getTypeValue(origin: object, val: string) {
58 + if (val && val.indexOf("{") === 0) {
59 + const path = val.replace("{", "").replace("}", "")
60 + return _get(origin, path)
61 + }
62 +
63 + return val
64 +}
65 +
66 +export function getThemeColors(colors: Record<ColorType, string>) {
67 + const colorActions: ColorAction[] = [
68 + { scene: "", handler: color => color },
69 + { scene: "Suppl", handler: color => exposure(color, 0.1) },
70 + { scene: "Hover", handler: color => exposure(color, 0.08) },
71 + { scene: "Pressed", handler: color => exposure(color, -0.05) }
72 + ]
73 +
74 + const themeColor: ThemeColor = {}
75 +
76 + for (const colorName in colors) {
77 + const colorValue = colors[colorName as ColorType]
78 +
79 + colorActions.forEach(action => {
80 + const colorKey: ColorKey = `${colorName as ColorType}Color${action.scene}`
81 + themeColor[colorKey] = action.handler(colorValue)
82 + })
83 + }
84 +
85 + return themeColor
86 +}
87 +
88 +/**
89 + * Generates an array of strings by expanding all values enclosed in parentheses
90 + *
91 + * @param input - The string to process, e.g. "brand-(seablue|green)-(10|20|50)"
92 + * @returns An array of expanded strings, e.g.
93 + * ["brand-seablue-10", "brand-seablue-20", ..., "brand-green-50"]
94 + */
95 +const PARENTHESIS_GROUP_REGEX = /\(([^)]+)\)/
96 +
97 +export function expandPattern(input: string): string[] {
98 + const match = input.match(PARENTHESIS_GROUP_REGEX)
99 + if (!match) {
100 + // If there are no more parentheses, returns the input as an array
101 + return [input]
102 + }
103 +
104 + // Expands the first group found
105 + const [fullMatch, options] = match
106 + const variants = options?.split("|") ?? [] // Splits the options by "|"
107 +
108 + // Replaces the first group with each option and calls recursively
109 + return variants.flatMap(option => expandPattern(input.replace(fullMatch, option)))
110 +}
customer_portal/src/views/DashboardViewPage.vue
+3 -356
@@ -70,132 +70,17 @@
70 </div>
71 </header>
72
73 - <!-- Main Content -->
73 <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
75 - <!-- Dashboard Header Bar -->
76 - <div class="mb-6 rounded-lg bg-white shadow">
77 - <div class="px-4 py-4 sm:px-6">
78 - <div class="flex flex-wrap items-center justify-between gap-4">
79 - <div class="flex items-center gap-3">
80 - <button
81 - @click="router.push('/dashboards')"
82 - class="rounded-md p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
83 - >
84 - <svg class="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85 - <path
86 - stroke-linecap="round"
87 - stroke-linejoin="round"
88 - stroke-width="2"
89 - d="M10 19l-7-7m0 0l7-7m-7 7h18"
90 - />
91 - </svg>
92 - </button>
93 - <div>
94 - <h2 class="text-lg font-semibold text-gray-900">
95 - {{ dashboardTitle || "Loading..." }}
96 - </h2>
97 - <p class="text-sm text-gray-500">{{ dashboardDescription }}</p>
98 - </div>
99 - </div>
100 - <div class="flex items-center gap-2">
101 - <button
102 - v-for="preset in timePresets"
103 - :key="preset.value"
104 - @click="selectTimerange(preset.value)"
105 - class="rounded-md px-3 py-1.5 text-sm font-medium transition-colors"
106 - :class="
107 - selectedTimerange === preset.value
108 - ? 'bg-indigo-600 text-white'
109 - : 'bg-gray-100 text-gray-700 hover:bg-gray-200'
110 - "
111 - >
112 - {{ preset.label }}
113 - </button>
114 - <button
115 - @click="fetchPanelData"
116 - :disabled="loading"
117 - class="ml-2 rounded-md bg-gray-100 p-1.5 text-gray-600 hover:bg-gray-200 disabled:opacity-50"
118 - >
119 - <svg
120 - class="h-4 w-4"
121 - :class="{ 'animate-spin': loading }"
122 - fill="none"
123 - stroke="currentColor"
124 - viewBox="0 0 24 24"
125 - >
126 - <path
127 - stroke-linecap="round"
128 - stroke-linejoin="round"
129 - stroke-width="2"
130 - d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
131 - />
132 - </svg>
133 - </button>
134 - </div>
135 - </div>
136 - </div>
137 - </div>
138 -
139 - <!-- Error -->
140 - <div v-if="errorMsg" class="mb-6 rounded-md border border-red-300 bg-red-50 p-4">
141 - <p class="text-sm text-red-700">{{ errorMsg }}</p>
142 - </div>
143 -
144 - <!-- Loading -->
145 - <div v-if="loading && !hasData" class="rounded-lg bg-white px-4 py-12 text-center shadow">
146 - <div class="mx-auto h-8 w-8 animate-spin rounded-full border-b-2 border-indigo-600"></div>
147 - <p class="mt-2 text-sm text-gray-500">Loading dashboard panels...</p>
148 - </div>
149 -
150 - <!-- Panels Grid -->
151 - <div v-if="hasData || (!loading && panels.length > 0)" class="panel-grid">
152 - <div v-for="panel in panels" :key="panel.id" :style="{ gridColumn: `span ${panel.w}` }">
153 - <!-- Stat Panel -->
154 - <div
155 - v-if="panel.type === 'stat'"
156 - class="cursor-pointer rounded-lg bg-white p-6 text-center shadow transition-shadow hover:shadow-md"
157 - @click="openEventSearch(panel.lucene || '*')"
158 - >
159 - <p class="text-xs font-medium tracking-wide text-gray-500 uppercase">{{ panel.title }}</p>
160 - <p class="mt-2 text-3xl font-bold" :style="{ color: accentColor }">
161 - {{ formatStatValue(panelResults[panel.id]?.value) }}
162 - </p>
163 - <p v-if="panelResults[panel.id]?.error" class="mt-1 text-xs text-red-500">
164 - {{ panelResults[panel.id].error }}
165 - </p>
166 - </div>
167 -
168 - <!-- Chart Panel -->
169 - <div v-else class="rounded-lg bg-white shadow">
170 - <div class="border-b border-gray-100 px-4 py-3">
171 - <h3 class="text-sm font-medium text-gray-700">{{ panel.title }}</h3>
172 - </div>
173 - <div class="p-2">
174 - <div :id="`chart-${panel.id}`" :style="{ height: `${panel.h}px`, width: '100%' }"></div>
175 - </div>
176 - <p v-if="panelResults[panel.id]?.error" class="px-4 pb-2 text-xs text-red-500">
177 - {{ panelResults[panel.id].error }}
178 - </p>
179 - </div>
180 - </div>
181 - </div>
74 + <DashboardViewer :dashboard-id />
75 </div>
76 </div>
77 </template>
78
79 <script setup lang="ts">
187 -import type { ECharts } from "echarts/core"
188 -import type { DashboardPanel, PanelResult } from "@/api/siem"
189 -import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from "vue"
80 +import { computed, ref } from "vue"
81 import { useRoute, useRouter } from "vue-router"
82 +import DashboardViewer from "@/components/dashboards/DashboardViewer.vue"
83 import { usePortalSettingsStore } from "@/stores/portalSettings"
192 -import { SiemAPI } from "@/api/siem"
193 -import { BarChart, LineChart, PieChart } from "echarts/charts"
194 -import { GridComponent, LegendComponent, TooltipComponent } from "echarts/components"
195 -import { init as echartsInit, use as echartsUse } from "echarts/core"
196 -import { CanvasRenderer } from "echarts/renderers"
197 -
198 -echartsUse([TooltipComponent, LegendComponent, GridComponent, PieChart, BarChart, LineChart, CanvasRenderer])
84
85 const route = useRoute()
86 const router = useRouter()
@@ -220,243 +105,5 @@ function logout() {
105 router.push("/login")
106 }
107
223 -// -- Dashboard state --
108 const dashboardId = computed(() => Number(route.params.id))
225 -
226 -const COLORS = [
227 - "#6366f1",
228 - "#818cf8",
229 - "#34d399",
230 - "#fbbf24",
231 - "#f87171",
232 - "#a78bfa",
233 - "#fb923c",
234 - "#2dd4bf",
235 - "#e879f9",
236 - "#94a3b8"
237 -]
238 -
239 -const accentColor = ref("#6366f1")
240 -const customerCode = ref("")
241 -const sourceName = ref("")
242 -const dashboardTitle = ref("")
243 -const dashboardDescription = ref("")
244 -const panels = ref<DashboardPanel[]>([])
245 -const panelResults = ref<Record<string, PanelResult>>({})
246 -const loading = ref(false)
247 -const errorMsg = ref("")
248 -const selectedTimerange = ref("24h")
249 -
250 -const timePresets = [
251 - { label: "1h", value: "1h" },
252 - { label: "6h", value: "6h" },
253 - { label: "24h", value: "24h" },
254 - { label: "7d", value: "7d" },
255 - { label: "30d", value: "30d" }
256 -]
257 -
258 -const hasData = computed(() => Object.keys(panelResults.value).length > 0)
259 -
260 -const chartInstances = ref<Map<string, ECharts>>(new Map())
261 -let resizeObservers: ResizeObserver[] = []
262 -
263 -function formatStatValue(value: number | null | undefined): string {
264 - if (value == null) return "—"
265 - if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`
266 - if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`
267 - return value.toLocaleString()
268 -}
269 -
270 -function selectTimerange(value: string) {
271 - selectedTimerange.value = value
272 - fetchPanelData()
273 -}
274 -
275 -// -- Drill-down to Event Search --
276 -function openEventSearch(luceneQuery: string) {
277 - const routeData = router.resolve({
278 - path: "/event-search",
279 - query: {
280 - customer_code: customerCode.value,
281 - source_name: sourceName.value,
282 - query: luceneQuery
283 - }
284 - })
285 - window.open(routeData.href, "_blank")
286 -}
287 -
288 -function buildDrilldownQuery(panel: DashboardPanel, clickedValue: string): string {
289 - const baseLucene = panel.lucene && panel.lucene !== "*" ? `(${panel.lucene})` : ""
290 - const fieldFilter = panel.field ? `${panel.field}:"${clickedValue}"` : ""
291 - return [baseLucene, fieldFilter].filter(Boolean).join(" AND ")
292 -}
293 -
294 -// -- Fetch panel data --
295 -async function fetchPanelData() {
296 - loading.value = true
297 - errorMsg.value = ""
298 - try {
299 - const res = await SiemAPI.getPanelData(dashboardId.value, selectedTimerange.value)
300 - if (res.success) {
301 - const tpl = res.template
302 - dashboardTitle.value = tpl.title
303 - dashboardDescription.value = tpl.description
304 - panels.value = tpl.panels
305 - accentColor.value = res.accent_color || "#6366f1"
306 - customerCode.value = res.customer_code
307 - sourceName.value = res.source_name
308 - panelResults.value = res.panels
309 - await nextTick()
310 - renderAllCharts()
311 - } else {
312 - errorMsg.value = res.message || "Failed to fetch panel data"
313 - }
314 - } catch (err: any) {
315 - errorMsg.value = err.response?.data?.detail || err.message || "Failed to fetch panel data"
316 - } finally {
317 - loading.value = false
318 - }
319 -}
320 -
321 -// -- ECharts options builders --
322 -function getHistogramOptions(result: PanelResult) {
323 - return {
324 - tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
325 - grid: { left: 50, right: 20, top: 10, bottom: 30 },
326 - xAxis: {
327 - type: "category",
328 - data: result.labels,
329 - axisLabel: { color: "#6b7280", fontSize: 10, rotate: result.labels.length > 20 ? 45 : 0 },
330 - axisLine: { lineStyle: { color: "#e5e7eb" } }
331 - },
332 - yAxis: {
333 - type: "value",
334 - axisLabel: { color: "#6b7280", fontSize: 10 },
335 - splitLine: { lineStyle: { color: "#f3f4f6" } }
336 - },
337 - series: [
338 - {
339 - type: "bar",
340 - data: result.data,
341 - itemStyle: { color: accentColor.value, borderRadius: [2, 2, 0, 0] }
342 - }
343 - ]
344 - }
345 -}
346 -
347 -function getPieOptions(result: PanelResult) {
348 - const data = result.labels.map((label, i) => ({ value: result.data[i], name: label }))
349 - return {
350 - tooltip: { trigger: "item", formatter: "{b}: <strong>{c}</strong> ({d}%)" },
351 - legend: {
352 - type: "scroll",
353 - orient: "vertical",
354 - right: 10,
355 - top: 20,
356 - bottom: 20,
357 - textStyle: { color: "#374151", fontSize: 11 }
358 - },
359 - series: [
360 - {
361 - type: "pie",
362 - radius: ["40%", "70%"],
363 - center: ["35%", "50%"],
364 - avoidLabelOverlap: true,
365 - itemStyle: { borderColor: "#ffffff", borderWidth: 2, borderRadius: 4 },
366 - label: { show: false },
367 - color: COLORS,
368 - data
369 - }
370 - ]
371 - }
372 -}
373 -
374 -function getBarHOptions(result: PanelResult) {
375 - return {
376 - tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
377 - grid: { left: 10, right: 30, top: 10, bottom: 10, containLabel: true },
378 - xAxis: {
379 - type: "value",
380 - axisLabel: { color: "#6b7280", fontSize: 10 },
381 - splitLine: { lineStyle: { color: "#f3f4f6" } }
382 - },
383 - yAxis: {
384 - type: "category",
385 - data: [...result.labels].reverse(),
386 - axisLabel: { color: "#6b7280", fontSize: 10, width: 180, overflow: "truncate" },
387 - axisLine: { lineStyle: { color: "#e5e7eb" } }
388 - },
389 - series: [
390 - {
391 - type: "bar",
392 - data: [...result.data].reverse(),
393 - itemStyle: { color: accentColor.value, borderRadius: [0, 2, 2, 0] }
394 - }
395 - ]
396 - }
397 -}
398 -
399 -function renderChart(panel: DashboardPanel) {
400 - const result = panelResults.value[panel.id]
401 - if (!result || panel.type === "stat") return
402 -
403 - const dom = document.getElementById(`chart-${panel.id}`)
404 - if (!dom) return
405 -
406 - let chart = chartInstances.value.get(panel.id)
407 - if (!chart) {
408 - chart = echartsInit(dom)
409 - chartInstances.value.set(panel.id, chart)
410 -
411 - // Click handler for drill-down to Event Search
412 - chart.on("click", (params: { name?: string }) => {
413 - if (!params.name) return
414 - const query = buildDrilldownQuery(panel, params.name)
415 - if (query) openEventSearch(query)
416 - })
417 -
418 - const observer = new ResizeObserver(() => chart?.resize())
419 - observer.observe(dom)
420 - resizeObservers.push(observer)
421 - }
422 -
423 - let options
424 - if (panel.type === "histogram") options = getHistogramOptions(result)
425 - else if (panel.type === "pie") options = getPieOptions(result)
426 - else if (panel.type === "bar_h") options = getBarHOptions(result)
427 -
428 - if (options) {
429 - chart.setOption(options, true)
430 - }
431 -}
432 -
433 -function renderAllCharts() {
434 - for (const panel of panels.value) {
435 - if (panel.type !== "stat") {
436 - renderChart(panel)
437 - }
438 - }
439 -}
440 -
441 -// -- Lifecycle --
442 -onMounted(() => {
443 - fetchPanelData()
444 -})
445 -
446 -onBeforeUnmount(() => {
447 - for (const chart of chartInstances.value.values()) {
448 - chart.dispose()
449 - }
450 - for (const obs of resizeObservers) {
451 - obs.disconnect()
452 - }
453 -})
109 </script>
455 -
456 -<style scoped>
457 -.panel-grid {
458 - display: grid;
459 - grid-template-columns: repeat(12, 1fr);
460 - gap: 16px;
461 -}
462 -</style>
customer_portal/src/views/DashboardsPage.vue
+3 -26
@@ -72,30 +72,6 @@
72
73 <!-- Main Content -->
74 <div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
75 - <!-- Customer Selector -->
76 - <div class="mb-6 rounded-lg bg-white shadow">
77 - <div class="px-4 py-5 sm:p-6">
78 - <div class="grid grid-cols-1 gap-4 md:grid-cols-4">
79 - <div>
80 - <label for="customer-select" class="block text-sm font-medium text-gray-700">
81 - Customer
82 - </label>
83 - <select
84 - id="customer-select"
85 - v-model="selectedCustomerCode"
86 - @change="onCustomerChange"
87 - class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
88 - >
89 - <option value="">Select a customer</option>
90 - <option v-for="code in customerCodes" :key="code" :value="code">
91 - {{ code }}
92 - </option>
93 - </select>
94 - </div>
95 - </div>
96 - </div>
97 - </div>
98 -
75 <!-- Error -->
76 <div v-if="error" class="mb-6 rounded-md border border-red-300 bg-red-50 p-4">
77 <p class="text-sm text-red-700">{{ error }}</p>
@@ -162,7 +138,7 @@
138 </td>
139 <td class="px-6 py-4 text-right text-sm whitespace-nowrap">
140 <router-link
165 - :to="`/dashboards/view/${dash.id}`"
141 + :to="{ name: 'DashboardView', params: { id: dash.id } }"
142 class="font-medium text-indigo-600 hover:text-indigo-900"
143 >
144 View Dashboard
@@ -229,12 +205,13 @@ const customerCodes = ref<string[]>([])
205 const selectedCustomerCode = ref("")
206 const dashboards = ref<EnabledDashboard[]>([])
207
208 +// TODO-FE: get customer code from login
209 async function loadCustomerCodes() {
210 try {
211 const response = await SiemAPI.getCustomerCodes()
212 customerCodes.value = response.customer_codes.filter(c => c !== "*")
213 // Auto-select if only one customer
237 - if (customerCodes.value.length === 1) {
214 + if (customerCodes.value.length) {
215 selectedCustomerCode.value = customerCodes.value[0]
216 loadDashboards(selectedCustomerCode.value)
217 }
customer_portal/vite.config.ts
+3 -1
@@ -21,7 +21,9 @@ export default defineConfig(({ mode }) => {
21 defineModel: true
22 }
23 }),
24 - VueDevTools(),
24 + VueDevTools({
25 + launchEditor: "cursor"
26 + }),
27 svgLoader()
28 ],
29 resolve: {
frontend/src/components/common/charts/ChartBar.vue
+1 -1
@@ -53,7 +53,7 @@ const chartOptions = computed<ApexOptions>(() => {
53 animations: { enabled: true },
54 events: {
55 dataPointSelection(_event, _chartContext, config) {
56 - const idx = config.dataPointIndex
56 + const idx = config?.dataPointIndex ?? 0
57 const name = props.labels[idx]
58 if (name) emit("itemClick", { name })
59 }
frontend/src/components/common/charts/ChartColumn.vue
+28 -4
@@ -57,7 +57,7 @@ const chartOptions = computed<ApexOptions>(() => {
57 animations: { enabled: true },
58 events: {
59 dataPointSelection(_event, _chartContext, config) {
60 - const idx = config.dataPointIndex
60 + const idx = config?.dataPointIndex ?? 0
61 const name = props.labels[idx]
62 if (name) emit("itemClick", { name })
63 }
@@ -65,7 +65,29 @@ const chartOptions = computed<ApexOptions>(() => {
65 },
66 responsive: [
67 {
68 - breakpoint: 1200,
68 + breakpoint: 1100,
69 + options: {
70 + xaxis: {
71 + tickAmount: 10,
72 + labels: {
73 + show: true
74 + }
75 + }
76 + }
77 + },
78 + {
79 + breakpoint: 750,
80 + options: {
81 + xaxis: {
82 + tickAmount: 5,
83 + labels: {
84 + show: true
85 + }
86 + }
87 + }
88 + },
89 + {
90 + breakpoint: 500,
91 options: {
92 xaxis: {
93 labels: {
@@ -102,9 +124,11 @@ const chartOptions = computed<ApexOptions>(() => {
124 ? [dayjs(label).format(dFormats.date), dayjs(label).format(dFormats.time)]
125 : label
126 }),
127 + tickAmount: 20,
128 labels: {
106 - style: { colors: fg, fontSize: "9px" },
107 - rotate: -65,
129 + show: true,
130 + style: { colors: fg, fontSize: "11px" },
131 + rotate: -50,
132 rotateAlways: true,
133 hideOverlappingLabels: true,
134 trim: false
frontend/src/components/common/charts/ChartPie.vue
+6 -4
@@ -50,7 +50,7 @@ const chartOptions = computed<ApexOptions>(() => {
50 animations: { enabled: true },
51 events: {
52 dataPointSelection(_event, _chartContext, config) {
53 - const idx = config.dataPointIndex
53 + const idx = config?.dataPointIndex ?? 0
54 const name = props.labels[idx]
55 if (name) emit("itemClick", { name })
56 }
@@ -73,7 +73,9 @@ const chartOptions = computed<ApexOptions>(() => {
73 size: "55%",
74 labels: {
75 show: true,
76 - value: { color: fg }
76 + name: { fontFamily: ff, fontSize: "12px", offsetY: -2 },
77 + value: { color: fg, fontFamily: ff, fontSize: "12px", offsetY: 0 },
78 + total: { color: fg, fontFamily: ff, fontSize: "12px", offsetY: 2 }
79 }
80 }
81 }
@@ -97,8 +99,8 @@ const chartOptions = computed<ApexOptions>(() => {
99 offsetX: -5
100 },
101 formatter: (seriesName, opts) => {
100 - const series = opts.w.globals.series as number[]
101 - const value = series[opts.seriesIndex] ?? 0
102 + const series = opts?.w.globals.series as number[]
103 + const value = series[opts?.seriesIndex ?? 0] ?? 0
104 const total = series.reduce((sum, v) => sum + v, 0)
105 const pct = total > 0 ? (value / total) * 100 : 0
106 return `${seriesName} - ${value} (${pct.toFixed(1)}%)`
frontend/src/components/dashboards/DashboardViewer.vue
+1 -1
@@ -58,7 +58,7 @@
58 :data="item.data.data"
59 :monochrome="item.panel.type === 'histogram'"
60 :labels-datetime="item.panel.type === 'histogram'"
61 - :height="`${item.panel.h}px`"
61 + :height="`${item.panel.type === 'histogram' ? `${item.panel.h + 100}px` : `${item.panel.h}px`}`"
62 @item-click="onChartItemClick(item.panel, $event.name)"
63 />
64