diff --git a/docs/member-statistics/README.md b/docs/member-statistics/README.md new file mode 100644 index 000000000..a172e231a --- /dev/null +++ b/docs/member-statistics/README.md @@ -0,0 +1,152 @@ + + +# Статистика участников: селективный перенос в PROD + +База: `master`, `cbcb5bffbaeb13a37176411a3ebda035618a5879`. +Источник: DEV [#381](https://github.com/PROCOLLAB-github/procollab/pull/381), +merge `19927ec09d337a69eee63c7050bc6d46f89f79a4`. + +Перенесена только финальная статистика участников. Merge-коммит DEV не включён +в историю PROD-ветки. Дополнительно усилен существующий тест независимости от +пагинации: изменение `membersTotalCount` не меняет глобальную статистику. + +## Поведение и контракт + +Данные проходят через HTTP adapter → repository → use-case → facade → +`MemberStatisticsCardComponent`. Прямого HTTP в `MembersComponent` нет. +Один запрос на открытие страницы: `GET /auth/public-users/stats/` без параметров. +Существующий interceptor преобразует snake_case в camelCase. + +| Ответ API | Модель Angular | Подпись | +| ------------------ | --------------- | ---------------- | +| `total` | `total` | Всего участников | +| `in_projects` | `inProjects` | В проектах | +| `in_programs` | `inPrograms` | В программах | +| `new_last_30_days` | `newLast30Days` | Новых за 30 дней | + +Repository проверяет четыре неотрицательных безопасных целых числа. +Загрузка и ошибка отображаются как `—`, реальные нули — как `0`. +Большие числа форматируются через `Intl.NumberFormat("ru-RU")`. +Технические тела ошибок не попадают в интерфейс. Есть доступные подписи +и статус загрузки; используются существующие `appIcon`. + +Статистика глобальная: fullname, skills, speciality, пагинация и смена layout +не меняют метрики и не вызывают повторный запрос. Старый shared `SoonCard` +не изменён; на этой странице он заменён статистикой. + +Backend-источник: DEV #756. Считаются активные legacy MEMBER: все; уникальные +лидеры или collaborators публичных нечерновых проектов; уникальные участники +нечерновых программ; зарегистрированные с включительной границы последних +30 суток. Backend использует `Exists`/`Count`, один SQL при 10/100/1000 людях. +В ответе только четыре числа, без персональных данных. Эти определения +Angular не пересчитывает. + +## Геометрия в браузере + +Проверка выполнена 27 сентября 2026 года в Chrome 154.0.8037.57 на Windows, +высота viewport 1000 px, DPR 1. Сравнивались отдельные сборки exact master и +ветки переноса с одинаковыми синтетическими участниками. Использованы настоящие +`MembersComponent`, фильтры, карточки и SCSS оболочки office; навигация и данные +заменены локальными fixtures. Это проверка интерфейса в браузере, а не обращение +к работающему PROD API. HTTP mapping проверен отдельными контрактными тестами. + +| Viewport, px | X сетки до | X сетки после | Ширина сетки до/после | Карточка W × H, px | Horizontal overflow | +| -----------: | ---------: | ------------: | --------------------: | -----------------: | ------------------: | +| 1440 | 365.828125 | 365.828125 | 684 | 157 × 280 | 0 | +| 1280 | 285.828125 | 285.828125 | 684 | 157 × 280 | 0 | +| 1024 | 201.5 | 201.5 | 580.5 | 157 × 280 | 0 | +| 768 | 16 | 16 | 721 | 721 × 172 | 0 | +| 414 | 16 | 16 | 367 | 367 × 172 | 0 | +| 390 | 16 | 16 | 343 | 343 × 172 | 0 | +| 375 | 16 | 16 | 328 | 328 × 172 | 0 | + +На desktop sidebar ровно **157 px**. Host и карточка имеют `width: 100%`, +`max-width: 157px`, `min-width: 0`, `box-sizing: border-box`. +Ширина и X-позиция сетки до/после совпадают без допуска на округление. + +При ширине меньше 1000 px используется тот же компонент, ширина равна ширине +списка, метрики расположены 2×2. Порядок: поиск → «Фильтры / Перейти в профиль» +→ статистика → участники. В стенде controls заканчиваются на Y=217, +статистика занимает Y=241…413, список начинается на Y=468. + +Для каждой из семи ширин проверены **0, 9, 63, 999, 1 248, 12 345, 999 999**, +загрузка и ошибка: 63 сценария плюс 7 обычных отображений. Во всех случаях +horizontal overflow и внутренний overflow карточки равны нулю, её размеры +остаются теми же. Проверено отсутствие `SoonCard`. +Поиск, изменение фильтров, добавление второй порции карточек и переключение +mobile → desktop сохраняют один вызов статистики и те же значения. +Пагинация настоящего `MembersUIInfoService` дополнительно проверена в Vitest. + +Полные машинные измерения: [browser-measurements.json](browser-measurements.json). + +## Проверки + +Окружение: Node 20.20.2, npm 10.8.2, зависимости из существующего lockfile. + +| Проверка | Результат | +| ------------------------------------------------------------ | --------------------------------------------------- | +| Целевые Vitest: facade, API contract, card, members, mobile | 30/30, 5 файлов | +| Полный `npm run test:ci` | 1925/1925, 398 файлов | +| `npm run lint:ts` | 0 ошибок; 6 предупреждений в неизменённых файлах | +| Stylelint всех `projects/**/*.scss`, включая изменённые SCSS | Пройден | +| Prettier изменённых файлов | Пройден | +| `npm run build:prod` | Пройден; initial bundle 1.40 MB, transfer 314.36 kB | +| `git diff --check` | Пройден | +| Браузерные проверки | 7 ширин, 70 отображений; исключений JavaScript нет | + +Полный suite выполнен перед дополнительными assertions пагинации; после их +добавления повторно прошли все 30 целевых тестов и ESLint изменённого теста. +Production-код после полного suite семантически не менялся. + +На Windows production build запускался с `npm_config_script_shell`, указывающим +на Git Bash: штатный `build:sprite` использует glob в одинарных кавычках. +Скрипты проекта не менялись, итоговый спрайт совпадает с master. +Сборка сохраняет предупреждения Angular/Sass, CommonJS и budgets в существующих +частях приложения; ошибок сборки нет. В тестах и обеих браузерных сборках есть +предупреждение NG0912 о существующих компонентах `IconComponent`. + +## Изменённые файлы приложения + +Префикс путей: `projects/social_platform/src/app/`. + +- `domain/member/member-statistics.model.ts` +- `domain/member/ports/member.repository.port.ts` +- `infrastructure/adapters/member/member-http.adapter.ts` +- `infrastructure/adapters/member/member-statistics-contract.spec.ts` +- `infrastructure/repository/member/member.repository.ts` +- `api/member/use-cases/get-member-statistics.use-case.ts` +- `api/member/facades/member-statistics.facade.ts` +- `api/member/facades/member-statistics.facade.spec.ts` +- `ui/pages/members/member-statistics-card/member-statistics-card.component.ts` +- `ui/pages/members/member-statistics-card/member-statistics-card.component.html` +- `ui/pages/members/member-statistics-card/member-statistics-card.component.scss` +- `ui/pages/members/member-statistics-card/member-statistics-card.component.spec.ts` +- `ui/pages/members/members.component.ts` +- `ui/pages/members/members.component.html` +- `ui/pages/members/members.component.scss` +- `ui/pages/members/members.component.spec.ts` +- `ui/pages/members/members-mobile.spec.ts` + +Дополнительно включены этот отчёт, JSON измерений и 20 скриншотов. Member cards, +skills UI, fullname search, mobile filters, shared SoonCard, зависимости, +workflows и React не изменены. + +## Скриншоты + +| Ширина | До | После | +| ------ | ------------------------------------- | ---------------------------------------- | +| 1440 | [master](screenshots/before-1440.jpg) | [статистика](screenshots/after-1440.jpg) | +| 1280 | [master](screenshots/before-1280.jpg) | [статистика](screenshots/after-1280.jpg) | +| 1024 | [master](screenshots/before-1024.jpg) | [статистика](screenshots/after-1024.jpg) | +| 768 | — | [статистика](screenshots/after-768.jpg) | +| 414 | — | [статистика](screenshots/after-414.jpg) | +| 390 | — | [статистика](screenshots/after-390.jpg) | +| 375 | — | [статистика](screenshots/after-375.jpg) | + +[0](screenshots/value-0.png) · [9](screenshots/value-9.png) · +[63](screenshots/value-63.png) · [999](screenshots/value-999.png) · +[1 248](screenshots/value-1248.png) · [12 345](screenshots/value-12345.png) · +[999 999](screenshots/value-999999.png). + +[Мобильный: большие числа](screenshots/mobile-large.jpg) · +[загрузка](screenshots/mobile-loading.jpg) · [ошибка](screenshots/mobile-error.jpg). diff --git a/docs/member-statistics/browser-measurements.json b/docs/member-statistics/browser-measurements.json new file mode 100644 index 000000000..bb5554f2c --- /dev/null +++ b/docs/member-statistics/browser-measurements.json @@ -0,0 +1,5524 @@ +{ + "base": "cbcb5bffbaeb13a37176411a3ebda035618a5879", + "environment": "Chromium / Windows; настоящие компоненты Angular, синтетические данные; оболочка office без навигации", + "viewports": [ + { + "width": 1440, + "before": { + "viewport": 1440, + "viewportClient": 1440, + "grid": { + "x": 365.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 1049.828125, + "bottom": 688 + }, + "sidebar": { + "x": 1069.828125, + "y": 142.59375, + "width": 157, + "height": 297.171875, + "right": 1226.828125, + "bottom": 439.765625 + }, + "card": null, + "host": null, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": [], + "metricColumns": null, + "metricRows": [], + "statisticsCalls": 0, + "font": true, + "soon": true + }, + "after": { + "viewport": 1440, + "viewportClient": 1440, + "grid": { + "x": 365.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 1049.828125, + "bottom": 688 + }, + "sidebar": { + "x": 1069.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1226.828125, + "bottom": 578.78125 + }, + "card": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "host": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "684", "214", "63"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1080.828125, + "y": 335.78125 + }, + { + "x": 1080.828125, + "y": 393.78125 + }, + { + "x": 1080.828125, + "y": 451.78125 + }, + { + "x": 1080.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + } + }, + { + "width": 1280, + "before": { + "viewport": 1280, + "viewportClient": 1280, + "grid": { + "x": 285.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 969.828125, + "bottom": 688 + }, + "sidebar": { + "x": 989.828125, + "y": 142.59375, + "width": 157, + "height": 297.171875, + "right": 1146.828125, + "bottom": 439.765625 + }, + "card": null, + "host": null, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": [], + "metricColumns": null, + "metricRows": [], + "statisticsCalls": 0, + "font": true, + "soon": true + }, + "after": { + "viewport": 1280, + "viewportClient": 1280, + "grid": { + "x": 285.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 969.828125, + "bottom": 688 + }, + "sidebar": { + "x": 989.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1146.828125, + "bottom": 578.78125 + }, + "card": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "host": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "684", "214", "63"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1000.828125, + "y": 335.78125 + }, + { + "x": 1000.828125, + "y": 393.78125 + }, + { + "x": 1000.828125, + "y": 451.78125 + }, + { + "x": 1000.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + } + }, + { + "width": 1024, + "before": { + "viewport": 1024, + "viewportClient": 1024, + "grid": { + "x": 201.5, + "y": 218, + "width": 580.5, + "height": 730, + "right": 782, + "bottom": 948 + }, + "sidebar": { + "x": 802, + "y": 142.59375, + "width": 157, + "height": 297.171875, + "right": 959, + "bottom": 439.765625 + }, + "card": null, + "host": null, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": [], + "metricColumns": null, + "metricRows": [], + "statisticsCalls": 0, + "font": true, + "soon": true + }, + "after": { + "viewport": 1024, + "viewportClient": 1024, + "grid": { + "x": 201.5, + "y": 218, + "width": 580.5, + "height": 730, + "right": 782, + "bottom": 948 + }, + "sidebar": { + "x": 802, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 959, + "bottom": 578.78125 + }, + "card": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "host": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "684", "214", "63"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 813, + "y": 335.78125 + }, + { + "x": 813, + "y": 393.78125 + }, + { + "x": 813, + "y": 451.78125 + }, + { + "x": 813, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + } + }, + { + "width": 768, + "before": { + "viewport": 768, + "viewportClient": 768, + "grid": { + "x": 16, + "y": 272, + "width": 721, + "height": 990, + "right": 737, + "bottom": 1262 + }, + "sidebar": null, + "card": null, + "host": null, + "controls": { + "x": 16, + "y": 173, + "width": 721, + "height": 44, + "right": 737, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 721, + "height": 44, + "right": 737, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": [], + "metricColumns": null, + "metricRows": [], + "statisticsCalls": 0, + "font": true, + "soon": true + }, + "after": { + "viewport": 768, + "viewportClient": 768, + "grid": { + "x": 16, + "y": 468, + "width": 721, + "height": 990, + "right": 737, + "bottom": 1458 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 721, + "height": 44, + "right": 737, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 721, + "height": 44, + "right": 737, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "684", "214", "63"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "343.5px 343.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 382.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 382.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + } + }, + { + "width": 414, + "before": { + "viewport": 414, + "viewportClient": 414, + "grid": { + "x": 16, + "y": 272, + "width": 367, + "height": 1770, + "right": 383, + "bottom": 2042 + }, + "sidebar": null, + "card": null, + "host": null, + "controls": { + "x": 16, + "y": 173, + "width": 367, + "height": 44, + "right": 383, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 367, + "height": 44, + "right": 383, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": [], + "metricColumns": null, + "metricRows": [], + "statisticsCalls": 0, + "font": true, + "soon": true + }, + "after": { + "viewport": 414, + "viewportClient": 414, + "grid": { + "x": 16, + "y": 468, + "width": 367, + "height": 1770, + "right": 383, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 367, + "height": 44, + "right": 383, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 367, + "height": 44, + "right": 383, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "684", "214", "63"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "166.5px 166.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 205.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 205.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + } + }, + { + "width": 390, + "before": { + "viewport": 390, + "viewportClient": 390, + "grid": { + "x": 16, + "y": 272, + "width": 343, + "height": 1770, + "right": 359, + "bottom": 2042 + }, + "sidebar": null, + "card": null, + "host": null, + "controls": { + "x": 16, + "y": 173, + "width": 343, + "height": 44, + "right": 359, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 343, + "height": 44, + "right": 359, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": [], + "metricColumns": null, + "metricRows": [], + "statisticsCalls": 0, + "font": true, + "soon": true + }, + "after": { + "viewport": 390, + "viewportClient": 390, + "grid": { + "x": 16, + "y": 468, + "width": 343, + "height": 1770, + "right": 359, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 343, + "height": 44, + "right": 359, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 343, + "height": 44, + "right": 359, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "684", "214", "63"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "154.5px 154.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 193.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 193.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + } + }, + { + "width": 375, + "before": { + "viewport": 375, + "viewportClient": 375, + "grid": { + "x": 16, + "y": 272, + "width": 328, + "height": 1770, + "right": 344, + "bottom": 2042 + }, + "sidebar": null, + "card": null, + "host": null, + "controls": { + "x": 16, + "y": 173, + "width": 328, + "height": 44, + "right": 344, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 328, + "height": 44, + "right": 344, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": [], + "metricColumns": null, + "metricRows": [], + "statisticsCalls": 0, + "font": true, + "soon": true + }, + "after": { + "viewport": 375, + "viewportClient": 375, + "grid": { + "x": 16, + "y": 468, + "width": 328, + "height": 1770, + "right": 344, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 328, + "height": 44, + "right": 344, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 328, + "height": 44, + "right": 344, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "684", "214", "63"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "147px 147px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 186, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 186, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + } + } + ], + "cases": [ + { + "width": 1440, + "scenario": "value-0", + "viewport": 1440, + "viewportClient": 1440, + "grid": { + "x": 365.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 1049.828125, + "bottom": 688 + }, + "sidebar": { + "x": 1069.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1226.828125, + "bottom": 578.78125 + }, + "card": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "host": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["0", "0", "0", "0"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1080.828125, + "y": 335.78125 + }, + { + "x": 1080.828125, + "y": 393.78125 + }, + { + "x": 1080.828125, + "y": 451.78125 + }, + { + "x": 1080.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1440, + "scenario": "value-9", + "viewport": 1440, + "viewportClient": 1440, + "grid": { + "x": 365.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 1049.828125, + "bottom": 688 + }, + "sidebar": { + "x": 1069.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1226.828125, + "bottom": 578.78125 + }, + "card": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "host": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["9", "9", "9", "9"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1080.828125, + "y": 335.78125 + }, + { + "x": 1080.828125, + "y": 393.78125 + }, + { + "x": 1080.828125, + "y": 451.78125 + }, + { + "x": 1080.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1440, + "scenario": "value-63", + "viewport": 1440, + "viewportClient": 1440, + "grid": { + "x": 365.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 1049.828125, + "bottom": 688 + }, + "sidebar": { + "x": 1069.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1226.828125, + "bottom": 578.78125 + }, + "card": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "host": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["63", "63", "63", "63"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1080.828125, + "y": 335.78125 + }, + { + "x": 1080.828125, + "y": 393.78125 + }, + { + "x": 1080.828125, + "y": 451.78125 + }, + { + "x": 1080.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1440, + "scenario": "value-999", + "viewport": 1440, + "viewportClient": 1440, + "grid": { + "x": 365.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 1049.828125, + "bottom": 688 + }, + "sidebar": { + "x": 1069.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1226.828125, + "bottom": 578.78125 + }, + "card": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "host": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["999", "999", "999", "999"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1080.828125, + "y": 335.78125 + }, + { + "x": 1080.828125, + "y": 393.78125 + }, + { + "x": 1080.828125, + "y": 451.78125 + }, + { + "x": 1080.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1440, + "scenario": "value-1248", + "viewport": 1440, + "viewportClient": 1440, + "grid": { + "x": 365.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 1049.828125, + "bottom": 688 + }, + "sidebar": { + "x": 1069.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1226.828125, + "bottom": 578.78125 + }, + "card": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "host": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "1 248", "1 248", "1 248"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1080.828125, + "y": 335.78125 + }, + { + "x": 1080.828125, + "y": 393.78125 + }, + { + "x": 1080.828125, + "y": 451.78125 + }, + { + "x": 1080.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1440, + "scenario": "value-12345", + "viewport": 1440, + "viewportClient": 1440, + "grid": { + "x": 365.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 1049.828125, + "bottom": 688 + }, + "sidebar": { + "x": 1069.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1226.828125, + "bottom": 578.78125 + }, + "card": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "host": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["12 345", "12 345", "12 345", "12 345"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1080.828125, + "y": 335.78125 + }, + { + "x": 1080.828125, + "y": 393.78125 + }, + { + "x": 1080.828125, + "y": 451.78125 + }, + { + "x": 1080.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1440, + "scenario": "value-999999", + "viewport": 1440, + "viewportClient": 1440, + "grid": { + "x": 365.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 1049.828125, + "bottom": 688 + }, + "sidebar": { + "x": 1069.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1226.828125, + "bottom": 578.78125 + }, + "card": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "host": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["999 999", "999 999", "999 999", "999 999"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1080.828125, + "y": 335.78125 + }, + { + "x": 1080.828125, + "y": 393.78125 + }, + { + "x": 1080.828125, + "y": 451.78125 + }, + { + "x": 1080.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1440, + "scenario": "loading", + "viewport": 1440, + "viewportClient": 1440, + "grid": { + "x": 365.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 1049.828125, + "bottom": 688 + }, + "sidebar": { + "x": 1069.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1226.828125, + "bottom": 578.78125 + }, + "card": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "host": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1080.828125, + "y": 335.78125 + }, + { + "x": 1080.828125, + "y": 393.78125 + }, + { + "x": 1080.828125, + "y": 451.78125 + }, + { + "x": 1080.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1440, + "scenario": "error", + "viewport": 1440, + "viewportClient": 1440, + "grid": { + "x": 365.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 1049.828125, + "bottom": 688 + }, + "sidebar": { + "x": 1069.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1226.828125, + "bottom": 578.78125 + }, + "card": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "host": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1080.828125, + "y": 335.78125 + }, + { + "x": 1080.828125, + "y": 393.78125 + }, + { + "x": 1080.828125, + "y": 451.78125 + }, + { + "x": 1080.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1280, + "scenario": "value-0", + "viewport": 1280, + "viewportClient": 1280, + "grid": { + "x": 285.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 969.828125, + "bottom": 688 + }, + "sidebar": { + "x": 989.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1146.828125, + "bottom": 578.78125 + }, + "card": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "host": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["0", "0", "0", "0"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1000.828125, + "y": 335.78125 + }, + { + "x": 1000.828125, + "y": 393.78125 + }, + { + "x": 1000.828125, + "y": 451.78125 + }, + { + "x": 1000.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1280, + "scenario": "value-9", + "viewport": 1280, + "viewportClient": 1280, + "grid": { + "x": 285.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 969.828125, + "bottom": 688 + }, + "sidebar": { + "x": 989.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1146.828125, + "bottom": 578.78125 + }, + "card": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "host": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["9", "9", "9", "9"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1000.828125, + "y": 335.78125 + }, + { + "x": 1000.828125, + "y": 393.78125 + }, + { + "x": 1000.828125, + "y": 451.78125 + }, + { + "x": 1000.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1280, + "scenario": "value-63", + "viewport": 1280, + "viewportClient": 1280, + "grid": { + "x": 285.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 969.828125, + "bottom": 688 + }, + "sidebar": { + "x": 989.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1146.828125, + "bottom": 578.78125 + }, + "card": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "host": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["63", "63", "63", "63"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1000.828125, + "y": 335.78125 + }, + { + "x": 1000.828125, + "y": 393.78125 + }, + { + "x": 1000.828125, + "y": 451.78125 + }, + { + "x": 1000.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1280, + "scenario": "value-999", + "viewport": 1280, + "viewportClient": 1280, + "grid": { + "x": 285.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 969.828125, + "bottom": 688 + }, + "sidebar": { + "x": 989.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1146.828125, + "bottom": 578.78125 + }, + "card": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "host": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["999", "999", "999", "999"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1000.828125, + "y": 335.78125 + }, + { + "x": 1000.828125, + "y": 393.78125 + }, + { + "x": 1000.828125, + "y": 451.78125 + }, + { + "x": 1000.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1280, + "scenario": "value-1248", + "viewport": 1280, + "viewportClient": 1280, + "grid": { + "x": 285.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 969.828125, + "bottom": 688 + }, + "sidebar": { + "x": 989.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1146.828125, + "bottom": 578.78125 + }, + "card": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "host": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "1 248", "1 248", "1 248"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1000.828125, + "y": 335.78125 + }, + { + "x": 1000.828125, + "y": 393.78125 + }, + { + "x": 1000.828125, + "y": 451.78125 + }, + { + "x": 1000.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1280, + "scenario": "value-12345", + "viewport": 1280, + "viewportClient": 1280, + "grid": { + "x": 285.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 969.828125, + "bottom": 688 + }, + "sidebar": { + "x": 989.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1146.828125, + "bottom": 578.78125 + }, + "card": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "host": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["12 345", "12 345", "12 345", "12 345"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1000.828125, + "y": 335.78125 + }, + { + "x": 1000.828125, + "y": 393.78125 + }, + { + "x": 1000.828125, + "y": 451.78125 + }, + { + "x": 1000.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1280, + "scenario": "value-999999", + "viewport": 1280, + "viewportClient": 1280, + "grid": { + "x": 285.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 969.828125, + "bottom": 688 + }, + "sidebar": { + "x": 989.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1146.828125, + "bottom": 578.78125 + }, + "card": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "host": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["999 999", "999 999", "999 999", "999 999"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1000.828125, + "y": 335.78125 + }, + { + "x": 1000.828125, + "y": 393.78125 + }, + { + "x": 1000.828125, + "y": 451.78125 + }, + { + "x": 1000.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1280, + "scenario": "loading", + "viewport": 1280, + "viewportClient": 1280, + "grid": { + "x": 285.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 969.828125, + "bottom": 688 + }, + "sidebar": { + "x": 989.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1146.828125, + "bottom": 578.78125 + }, + "card": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "host": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1000.828125, + "y": 335.78125 + }, + { + "x": 1000.828125, + "y": 393.78125 + }, + { + "x": 1000.828125, + "y": 451.78125 + }, + { + "x": 1000.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1280, + "scenario": "error", + "viewport": 1280, + "viewportClient": 1280, + "grid": { + "x": 285.828125, + "y": 218, + "width": 684, + "height": 470, + "right": 969.828125, + "bottom": 688 + }, + "sidebar": { + "x": 989.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1146.828125, + "bottom": 578.78125 + }, + "card": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "host": { + "x": 989.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1146.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1000.828125, + "y": 335.78125 + }, + { + "x": 1000.828125, + "y": 393.78125 + }, + { + "x": 1000.828125, + "y": 451.78125 + }, + { + "x": 1000.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1024, + "scenario": "value-0", + "viewport": 1024, + "viewportClient": 1024, + "grid": { + "x": 201.5, + "y": 218, + "width": 580.5, + "height": 730, + "right": 782, + "bottom": 948 + }, + "sidebar": { + "x": 802, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 959, + "bottom": 578.78125 + }, + "card": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "host": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["0", "0", "0", "0"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 813, + "y": 335.78125 + }, + { + "x": 813, + "y": 393.78125 + }, + { + "x": 813, + "y": 451.78125 + }, + { + "x": 813, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1024, + "scenario": "value-9", + "viewport": 1024, + "viewportClient": 1024, + "grid": { + "x": 201.5, + "y": 218, + "width": 580.5, + "height": 730, + "right": 782, + "bottom": 948 + }, + "sidebar": { + "x": 802, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 959, + "bottom": 578.78125 + }, + "card": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "host": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["9", "9", "9", "9"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 813, + "y": 335.78125 + }, + { + "x": 813, + "y": 393.78125 + }, + { + "x": 813, + "y": 451.78125 + }, + { + "x": 813, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1024, + "scenario": "value-63", + "viewport": 1024, + "viewportClient": 1024, + "grid": { + "x": 201.5, + "y": 218, + "width": 580.5, + "height": 730, + "right": 782, + "bottom": 948 + }, + "sidebar": { + "x": 802, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 959, + "bottom": 578.78125 + }, + "card": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "host": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["63", "63", "63", "63"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 813, + "y": 335.78125 + }, + { + "x": 813, + "y": 393.78125 + }, + { + "x": 813, + "y": 451.78125 + }, + { + "x": 813, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1024, + "scenario": "value-999", + "viewport": 1024, + "viewportClient": 1024, + "grid": { + "x": 201.5, + "y": 218, + "width": 580.5, + "height": 730, + "right": 782, + "bottom": 948 + }, + "sidebar": { + "x": 802, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 959, + "bottom": 578.78125 + }, + "card": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "host": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["999", "999", "999", "999"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 813, + "y": 335.78125 + }, + { + "x": 813, + "y": 393.78125 + }, + { + "x": 813, + "y": 451.78125 + }, + { + "x": 813, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1024, + "scenario": "value-1248", + "viewport": 1024, + "viewportClient": 1024, + "grid": { + "x": 201.5, + "y": 218, + "width": 580.5, + "height": 730, + "right": 782, + "bottom": 948 + }, + "sidebar": { + "x": 802, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 959, + "bottom": 578.78125 + }, + "card": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "host": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "1 248", "1 248", "1 248"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 813, + "y": 335.78125 + }, + { + "x": 813, + "y": 393.78125 + }, + { + "x": 813, + "y": 451.78125 + }, + { + "x": 813, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1024, + "scenario": "value-12345", + "viewport": 1024, + "viewportClient": 1024, + "grid": { + "x": 201.5, + "y": 218, + "width": 580.5, + "height": 730, + "right": 782, + "bottom": 948 + }, + "sidebar": { + "x": 802, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 959, + "bottom": 578.78125 + }, + "card": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "host": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["12 345", "12 345", "12 345", "12 345"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 813, + "y": 335.78125 + }, + { + "x": 813, + "y": 393.78125 + }, + { + "x": 813, + "y": 451.78125 + }, + { + "x": 813, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1024, + "scenario": "value-999999", + "viewport": 1024, + "viewportClient": 1024, + "grid": { + "x": 201.5, + "y": 218, + "width": 580.5, + "height": 730, + "right": 782, + "bottom": 948 + }, + "sidebar": { + "x": 802, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 959, + "bottom": 578.78125 + }, + "card": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "host": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["999 999", "999 999", "999 999", "999 999"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 813, + "y": 335.78125 + }, + { + "x": 813, + "y": 393.78125 + }, + { + "x": 813, + "y": 451.78125 + }, + { + "x": 813, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1024, + "scenario": "loading", + "viewport": 1024, + "viewportClient": 1024, + "grid": { + "x": 201.5, + "y": 218, + "width": 580.5, + "height": 730, + "right": 782, + "bottom": 948 + }, + "sidebar": { + "x": 802, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 959, + "bottom": 578.78125 + }, + "card": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "host": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 813, + "y": 335.78125 + }, + { + "x": 813, + "y": 393.78125 + }, + { + "x": 813, + "y": 451.78125 + }, + { + "x": 813, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 1024, + "scenario": "error", + "viewport": 1024, + "viewportClient": 1024, + "grid": { + "x": 201.5, + "y": 218, + "width": 580.5, + "height": 730, + "right": 782, + "bottom": 948 + }, + "sidebar": { + "x": 802, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 959, + "bottom": 578.78125 + }, + "card": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "host": { + "x": 802, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 959, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 813, + "y": 335.78125 + }, + { + "x": 813, + "y": 393.78125 + }, + { + "x": 813, + "y": 451.78125 + }, + { + "x": 813, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 768, + "scenario": "value-0", + "viewport": 768, + "viewportClient": 768, + "grid": { + "x": 16, + "y": 468, + "width": 721, + "height": 990, + "right": 737, + "bottom": 1458 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 721, + "height": 44, + "right": 737, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 721, + "height": 44, + "right": 737, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["0", "0", "0", "0"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "343.5px 343.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 382.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 382.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 768, + "scenario": "value-9", + "viewport": 768, + "viewportClient": 768, + "grid": { + "x": 16, + "y": 468, + "width": 721, + "height": 990, + "right": 737, + "bottom": 1458 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 721, + "height": 44, + "right": 737, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 721, + "height": 44, + "right": 737, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["9", "9", "9", "9"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "343.5px 343.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 382.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 382.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 768, + "scenario": "value-63", + "viewport": 768, + "viewportClient": 768, + "grid": { + "x": 16, + "y": 468, + "width": 721, + "height": 990, + "right": 737, + "bottom": 1458 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 721, + "height": 44, + "right": 737, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 721, + "height": 44, + "right": 737, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["63", "63", "63", "63"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "343.5px 343.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 382.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 382.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 768, + "scenario": "value-999", + "viewport": 768, + "viewportClient": 768, + "grid": { + "x": 16, + "y": 468, + "width": 721, + "height": 990, + "right": 737, + "bottom": 1458 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 721, + "height": 44, + "right": 737, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 721, + "height": 44, + "right": 737, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["999", "999", "999", "999"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "343.5px 343.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 382.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 382.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 768, + "scenario": "value-1248", + "viewport": 768, + "viewportClient": 768, + "grid": { + "x": 16, + "y": 468, + "width": 721, + "height": 990, + "right": 737, + "bottom": 1458 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 721, + "height": 44, + "right": 737, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 721, + "height": 44, + "right": 737, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "1 248", "1 248", "1 248"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "343.5px 343.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 382.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 382.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 768, + "scenario": "value-12345", + "viewport": 768, + "viewportClient": 768, + "grid": { + "x": 16, + "y": 468, + "width": 721, + "height": 990, + "right": 737, + "bottom": 1458 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 721, + "height": 44, + "right": 737, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 721, + "height": 44, + "right": 737, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["12 345", "12 345", "12 345", "12 345"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "343.5px 343.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 382.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 382.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 768, + "scenario": "value-999999", + "viewport": 768, + "viewportClient": 768, + "grid": { + "x": 16, + "y": 468, + "width": 721, + "height": 990, + "right": 737, + "bottom": 1458 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 721, + "height": 44, + "right": 737, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 721, + "height": 44, + "right": 737, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["999 999", "999 999", "999 999", "999 999"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "343.5px 343.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 382.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 382.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 768, + "scenario": "loading", + "viewport": 768, + "viewportClient": 768, + "grid": { + "x": 16, + "y": 468, + "width": 721, + "height": 990, + "right": 737, + "bottom": 1458 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 721, + "height": 44, + "right": 737, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 721, + "height": 44, + "right": 737, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "343.5px 343.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 382.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 382.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 768, + "scenario": "error", + "viewport": 768, + "viewportClient": 768, + "grid": { + "x": 16, + "y": 468, + "width": 721, + "height": 990, + "right": 737, + "bottom": 1458 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 721, + "height": 172, + "right": 737, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 721, + "height": 44, + "right": 737, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 721, + "height": 44, + "right": 737, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "343.5px 343.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 382.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 382.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 414, + "scenario": "value-0", + "viewport": 414, + "viewportClient": 414, + "grid": { + "x": 16, + "y": 468, + "width": 367, + "height": 1770, + "right": 383, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 367, + "height": 44, + "right": 383, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 367, + "height": 44, + "right": 383, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["0", "0", "0", "0"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "166.5px 166.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 205.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 205.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 414, + "scenario": "value-9", + "viewport": 414, + "viewportClient": 414, + "grid": { + "x": 16, + "y": 468, + "width": 367, + "height": 1770, + "right": 383, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 367, + "height": 44, + "right": 383, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 367, + "height": 44, + "right": 383, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["9", "9", "9", "9"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "166.5px 166.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 205.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 205.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 414, + "scenario": "value-63", + "viewport": 414, + "viewportClient": 414, + "grid": { + "x": 16, + "y": 468, + "width": 367, + "height": 1770, + "right": 383, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 367, + "height": 44, + "right": 383, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 367, + "height": 44, + "right": 383, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["63", "63", "63", "63"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "166.5px 166.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 205.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 205.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 414, + "scenario": "value-999", + "viewport": 414, + "viewportClient": 414, + "grid": { + "x": 16, + "y": 468, + "width": 367, + "height": 1770, + "right": 383, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 367, + "height": 44, + "right": 383, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 367, + "height": 44, + "right": 383, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["999", "999", "999", "999"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "166.5px 166.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 205.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 205.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 414, + "scenario": "value-1248", + "viewport": 414, + "viewportClient": 414, + "grid": { + "x": 16, + "y": 468, + "width": 367, + "height": 1770, + "right": 383, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 367, + "height": 44, + "right": 383, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 367, + "height": 44, + "right": 383, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "1 248", "1 248", "1 248"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "166.5px 166.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 205.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 205.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 414, + "scenario": "value-12345", + "viewport": 414, + "viewportClient": 414, + "grid": { + "x": 16, + "y": 468, + "width": 367, + "height": 1770, + "right": 383, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 367, + "height": 44, + "right": 383, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 367, + "height": 44, + "right": 383, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["12 345", "12 345", "12 345", "12 345"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "166.5px 166.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 205.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 205.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 414, + "scenario": "value-999999", + "viewport": 414, + "viewportClient": 414, + "grid": { + "x": 16, + "y": 468, + "width": 367, + "height": 1770, + "right": 383, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 367, + "height": 44, + "right": 383, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 367, + "height": 44, + "right": 383, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["999 999", "999 999", "999 999", "999 999"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "166.5px 166.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 205.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 205.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 414, + "scenario": "loading", + "viewport": 414, + "viewportClient": 414, + "grid": { + "x": 16, + "y": 468, + "width": 367, + "height": 1770, + "right": 383, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 367, + "height": 44, + "right": 383, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 367, + "height": 44, + "right": 383, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "166.5px 166.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 205.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 205.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 414, + "scenario": "error", + "viewport": 414, + "viewportClient": 414, + "grid": { + "x": 16, + "y": 468, + "width": 367, + "height": 1770, + "right": 383, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 367, + "height": 172, + "right": 383, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 367, + "height": 44, + "right": 383, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 367, + "height": 44, + "right": 383, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "166.5px 166.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 205.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 205.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 390, + "scenario": "value-0", + "viewport": 390, + "viewportClient": 390, + "grid": { + "x": 16, + "y": 468, + "width": 343, + "height": 1770, + "right": 359, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 343, + "height": 44, + "right": 359, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 343, + "height": 44, + "right": 359, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["0", "0", "0", "0"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "154.5px 154.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 193.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 193.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 390, + "scenario": "value-9", + "viewport": 390, + "viewportClient": 390, + "grid": { + "x": 16, + "y": 468, + "width": 343, + "height": 1770, + "right": 359, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 343, + "height": 44, + "right": 359, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 343, + "height": 44, + "right": 359, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["9", "9", "9", "9"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "154.5px 154.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 193.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 193.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 390, + "scenario": "value-63", + "viewport": 390, + "viewportClient": 390, + "grid": { + "x": 16, + "y": 468, + "width": 343, + "height": 1770, + "right": 359, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 343, + "height": 44, + "right": 359, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 343, + "height": 44, + "right": 359, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["63", "63", "63", "63"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "154.5px 154.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 193.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 193.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 390, + "scenario": "value-999", + "viewport": 390, + "viewportClient": 390, + "grid": { + "x": 16, + "y": 468, + "width": 343, + "height": 1770, + "right": 359, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 343, + "height": 44, + "right": 359, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 343, + "height": 44, + "right": 359, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["999", "999", "999", "999"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "154.5px 154.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 193.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 193.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 390, + "scenario": "value-1248", + "viewport": 390, + "viewportClient": 390, + "grid": { + "x": 16, + "y": 468, + "width": 343, + "height": 1770, + "right": 359, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 343, + "height": 44, + "right": 359, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 343, + "height": 44, + "right": 359, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "1 248", "1 248", "1 248"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "154.5px 154.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 193.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 193.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 390, + "scenario": "value-12345", + "viewport": 390, + "viewportClient": 390, + "grid": { + "x": 16, + "y": 468, + "width": 343, + "height": 1770, + "right": 359, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 343, + "height": 44, + "right": 359, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 343, + "height": 44, + "right": 359, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["12 345", "12 345", "12 345", "12 345"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "154.5px 154.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 193.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 193.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 390, + "scenario": "value-999999", + "viewport": 390, + "viewportClient": 390, + "grid": { + "x": 16, + "y": 468, + "width": 343, + "height": 1770, + "right": 359, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 343, + "height": 44, + "right": 359, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 343, + "height": 44, + "right": 359, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["999 999", "999 999", "999 999", "999 999"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "154.5px 154.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 193.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 193.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 390, + "scenario": "loading", + "viewport": 390, + "viewportClient": 390, + "grid": { + "x": 16, + "y": 468, + "width": 343, + "height": 1770, + "right": 359, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 343, + "height": 44, + "right": 359, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 343, + "height": 44, + "right": 359, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "154.5px 154.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 193.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 193.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 390, + "scenario": "error", + "viewport": 390, + "viewportClient": 390, + "grid": { + "x": 16, + "y": 468, + "width": 343, + "height": 1770, + "right": 359, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 343, + "height": 44, + "right": 359, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 343, + "height": 44, + "right": 359, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "154.5px 154.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 193.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 193.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 375, + "scenario": "value-0", + "viewport": 375, + "viewportClient": 375, + "grid": { + "x": 16, + "y": 468, + "width": 328, + "height": 1770, + "right": 344, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 328, + "height": 44, + "right": 344, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 328, + "height": 44, + "right": 344, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["0", "0", "0", "0"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "147px 147px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 186, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 186, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 375, + "scenario": "value-9", + "viewport": 375, + "viewportClient": 375, + "grid": { + "x": 16, + "y": 468, + "width": 328, + "height": 1770, + "right": 344, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 328, + "height": 44, + "right": 344, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 328, + "height": 44, + "right": 344, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["9", "9", "9", "9"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "147px 147px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 186, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 186, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 375, + "scenario": "value-63", + "viewport": 375, + "viewportClient": 375, + "grid": { + "x": 16, + "y": 468, + "width": 328, + "height": 1770, + "right": 344, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 328, + "height": 44, + "right": 344, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 328, + "height": 44, + "right": 344, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["63", "63", "63", "63"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "147px 147px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 186, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 186, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 375, + "scenario": "value-999", + "viewport": 375, + "viewportClient": 375, + "grid": { + "x": 16, + "y": 468, + "width": 328, + "height": 1770, + "right": 344, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 328, + "height": 44, + "right": 344, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 328, + "height": 44, + "right": 344, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["999", "999", "999", "999"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "147px 147px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 186, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 186, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 375, + "scenario": "value-1248", + "viewport": 375, + "viewportClient": 375, + "grid": { + "x": 16, + "y": 468, + "width": 328, + "height": 1770, + "right": 344, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 328, + "height": 44, + "right": 344, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 328, + "height": 44, + "right": 344, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "1 248", "1 248", "1 248"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "147px 147px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 186, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 186, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 375, + "scenario": "value-12345", + "viewport": 375, + "viewportClient": 375, + "grid": { + "x": 16, + "y": 468, + "width": 328, + "height": 1770, + "right": 344, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 328, + "height": 44, + "right": 344, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 328, + "height": 44, + "right": 344, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["12 345", "12 345", "12 345", "12 345"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "147px 147px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 186, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 186, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 375, + "scenario": "value-999999", + "viewport": 375, + "viewportClient": 375, + "grid": { + "x": 16, + "y": 468, + "width": 328, + "height": 1770, + "right": 344, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 328, + "height": 44, + "right": 344, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 328, + "height": 44, + "right": 344, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["999 999", "999 999", "999 999", "999 999"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "147px 147px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 186, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 186, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 375, + "scenario": "loading", + "viewport": 375, + "viewportClient": 375, + "grid": { + "x": 16, + "y": 468, + "width": 328, + "height": 1770, + "right": 344, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 328, + "height": 44, + "right": 344, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 328, + "height": 44, + "right": 344, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "147px 147px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 186, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 186, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + { + "width": 375, + "scenario": "error", + "viewport": 375, + "viewportClient": 375, + "grid": { + "x": 16, + "y": 468, + "width": 328, + "height": 1770, + "right": 344, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 328, + "height": 172, + "right": 344, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 328, + "height": 44, + "right": 344, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 328, + "height": 44, + "right": 344, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["—", "—", "—", "—"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "147px 147px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 186, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 186, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + } + ], + "independence": { + "initial": { + "viewport": 390, + "viewportClient": 390, + "grid": { + "x": 16, + "y": 468, + "width": 343, + "height": 1770, + "right": 359, + "bottom": 2238 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 343, + "height": 44, + "right": 359, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 343, + "height": 44, + "right": 359, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "684", "214", "63"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "154.5px 154.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 193.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 193.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + "afterSearchFiltersAndAppend": { + "viewport": 390, + "viewportClient": 390, + "grid": { + "x": 16, + "y": 468, + "width": 343, + "height": 3590, + "right": 359, + "bottom": 4058 + }, + "sidebar": null, + "card": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "host": { + "x": 16, + "y": 241, + "width": 343, + "height": 172, + "right": 359, + "bottom": 413 + }, + "controls": { + "x": 16, + "y": 173, + "width": 343, + "height": 44, + "right": 359, + "bottom": 217 + }, + "search": { + "x": 16, + "y": 109, + "width": 343, + "height": 44, + "right": 359, + "bottom": 153 + }, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "684", "214", "63"], + "maxWidth": "none", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "154.5px 154.5px", + "metricRows": [ + { + "x": 27, + "y": 278 + }, + { + "x": 193.5, + "y": 278 + }, + { + "x": 27, + "y": 344 + }, + { + "x": 193.5, + "y": 344 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + "afterResize": { + "viewport": 1440, + "viewportClient": 1440, + "grid": { + "x": 365.828125, + "y": 218, + "width": 684, + "height": 990, + "right": 1049.828125, + "bottom": 1208 + }, + "sidebar": { + "x": 1069.828125, + "y": 142.59375, + "width": 157, + "height": 436.1875, + "right": 1226.828125, + "bottom": 578.78125 + }, + "card": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "host": { + "x": 1069.828125, + "y": 298.78125, + "width": 157, + "height": 280, + "right": 1226.828125, + "bottom": 578.78125 + }, + "controls": null, + "search": null, + "overflow": 0, + "cardOverflow": 0, + "values": ["1 248", "684", "214", "63"], + "maxWidth": "157px", + "minWidth": "0px", + "boxSizing": "border-box", + "metricColumns": "none", + "metricRows": [ + { + "x": 1080.828125, + "y": 335.78125 + }, + { + "x": 1080.828125, + "y": 393.78125 + }, + { + "x": 1080.828125, + "y": 451.78125 + }, + { + "x": 1080.828125, + "y": 509.78125 + } + ], + "statisticsCalls": 1, + "font": true, + "soon": false + }, + "memberCards": 14 + }, + "errors": [], + "browser": "154.0.8037.57" +} diff --git a/docs/member-statistics/screenshots/after-1024.jpg b/docs/member-statistics/screenshots/after-1024.jpg new file mode 100644 index 000000000..33593949d Binary files /dev/null and b/docs/member-statistics/screenshots/after-1024.jpg differ diff --git a/docs/member-statistics/screenshots/after-1280.jpg b/docs/member-statistics/screenshots/after-1280.jpg new file mode 100644 index 000000000..97c9cd7b6 Binary files /dev/null and b/docs/member-statistics/screenshots/after-1280.jpg differ diff --git a/docs/member-statistics/screenshots/after-1440.jpg b/docs/member-statistics/screenshots/after-1440.jpg new file mode 100644 index 000000000..66ea6b28a Binary files /dev/null and b/docs/member-statistics/screenshots/after-1440.jpg differ diff --git a/docs/member-statistics/screenshots/after-375.jpg b/docs/member-statistics/screenshots/after-375.jpg new file mode 100644 index 000000000..30684ac50 Binary files /dev/null and b/docs/member-statistics/screenshots/after-375.jpg differ diff --git a/docs/member-statistics/screenshots/after-390.jpg b/docs/member-statistics/screenshots/after-390.jpg new file mode 100644 index 000000000..43e569fa4 Binary files /dev/null and b/docs/member-statistics/screenshots/after-390.jpg differ diff --git a/docs/member-statistics/screenshots/after-414.jpg b/docs/member-statistics/screenshots/after-414.jpg new file mode 100644 index 000000000..d044bc2bb Binary files /dev/null and b/docs/member-statistics/screenshots/after-414.jpg differ diff --git a/docs/member-statistics/screenshots/after-768.jpg b/docs/member-statistics/screenshots/after-768.jpg new file mode 100644 index 000000000..b9949312c Binary files /dev/null and b/docs/member-statistics/screenshots/after-768.jpg differ diff --git a/docs/member-statistics/screenshots/before-1024.jpg b/docs/member-statistics/screenshots/before-1024.jpg new file mode 100644 index 000000000..e56d49b96 Binary files /dev/null and b/docs/member-statistics/screenshots/before-1024.jpg differ diff --git a/docs/member-statistics/screenshots/before-1280.jpg b/docs/member-statistics/screenshots/before-1280.jpg new file mode 100644 index 000000000..8f8d27995 Binary files /dev/null and b/docs/member-statistics/screenshots/before-1280.jpg differ diff --git a/docs/member-statistics/screenshots/before-1440.jpg b/docs/member-statistics/screenshots/before-1440.jpg new file mode 100644 index 000000000..05e48720b Binary files /dev/null and b/docs/member-statistics/screenshots/before-1440.jpg differ diff --git a/docs/member-statistics/screenshots/mobile-error.jpg b/docs/member-statistics/screenshots/mobile-error.jpg new file mode 100644 index 000000000..f3677b34f Binary files /dev/null and b/docs/member-statistics/screenshots/mobile-error.jpg differ diff --git a/docs/member-statistics/screenshots/mobile-large.jpg b/docs/member-statistics/screenshots/mobile-large.jpg new file mode 100644 index 000000000..c3e0de8ea Binary files /dev/null and b/docs/member-statistics/screenshots/mobile-large.jpg differ diff --git a/docs/member-statistics/screenshots/mobile-loading.jpg b/docs/member-statistics/screenshots/mobile-loading.jpg new file mode 100644 index 000000000..f3677b34f Binary files /dev/null and b/docs/member-statistics/screenshots/mobile-loading.jpg differ diff --git a/docs/member-statistics/screenshots/value-0.png b/docs/member-statistics/screenshots/value-0.png new file mode 100644 index 000000000..ccef4de1a Binary files /dev/null and b/docs/member-statistics/screenshots/value-0.png differ diff --git a/docs/member-statistics/screenshots/value-12345.png b/docs/member-statistics/screenshots/value-12345.png new file mode 100644 index 000000000..863a7edbc Binary files /dev/null and b/docs/member-statistics/screenshots/value-12345.png differ diff --git a/docs/member-statistics/screenshots/value-1248.png b/docs/member-statistics/screenshots/value-1248.png new file mode 100644 index 000000000..36c4951cc Binary files /dev/null and b/docs/member-statistics/screenshots/value-1248.png differ diff --git a/docs/member-statistics/screenshots/value-63.png b/docs/member-statistics/screenshots/value-63.png new file mode 100644 index 000000000..7933a5257 Binary files /dev/null and b/docs/member-statistics/screenshots/value-63.png differ diff --git a/docs/member-statistics/screenshots/value-9.png b/docs/member-statistics/screenshots/value-9.png new file mode 100644 index 000000000..5fef6adb4 Binary files /dev/null and b/docs/member-statistics/screenshots/value-9.png differ diff --git a/docs/member-statistics/screenshots/value-999.png b/docs/member-statistics/screenshots/value-999.png new file mode 100644 index 000000000..87fc42839 Binary files /dev/null and b/docs/member-statistics/screenshots/value-999.png differ diff --git a/docs/member-statistics/screenshots/value-999999.png b/docs/member-statistics/screenshots/value-999999.png new file mode 100644 index 000000000..68db6853f Binary files /dev/null and b/docs/member-statistics/screenshots/value-999999.png differ diff --git a/projects/social_platform/src/app/api/member/facades/member-statistics.facade.spec.ts b/projects/social_platform/src/app/api/member/facades/member-statistics.facade.spec.ts new file mode 100644 index 000000000..4556d8234 --- /dev/null +++ b/projects/social_platform/src/app/api/member/facades/member-statistics.facade.spec.ts @@ -0,0 +1,74 @@ +/** @format */ + +import { TestBed } from "@angular/core/testing"; +import { Subject, throwError } from "rxjs"; +import { MemberStatistics } from "@domain/member/member-statistics.model"; +import { MemberRepositoryPort } from "@domain/member/ports/member.repository.port"; +import { fail, ok, Result } from "@domain/shared/result.type"; +import { GetMemberStatisticsUseCase } from "../use-cases/get-member-statistics.use-case"; +import { MemberStatisticsFacade } from "./member-statistics.facade"; + +describe("Состояние глобальной статистики участников", () => { + const data = { total: 1248, inProjects: 684, inPrograms: 214, newLast30Days: 63 }; + let response: Subject>; + let execute: ReturnType; + beforeEach(() => { + response = new Subject(); + execute = vi.fn(() => response); + TestBed.configureTestingModule({ + providers: [ + MemberStatisticsFacade, + { provide: GetMemberStatisticsUseCase, useValue: { execute } }, + ], + }); + }); + + it("остаётся loading до ответа и выполняет только один запрос за жизнь страницы", () => { + const facade = TestBed.inject(MemberStatisticsFacade); + expect(facade.state().status).toBe("loading"); + facade.load(); + facade.load(); + expect(execute).toHaveBeenCalledOnce(); + response.next(ok(data)); + expect(facade.state()).toEqual({ status: "success", data }); + }); + + it("сохраняет ошибку и не запускает бесконечные повторы", () => { + const facade = TestBed.inject(MemberStatisticsFacade); + facade.load(); + response.next(fail("member_statistics_error")); + facade.load(); + expect(facade.state().status).toBe("failure"); + expect(execute).toHaveBeenCalledOnce(); + }); + + it("отписывается при уходе со страницы: поздний ответ не меняет прежнее состояние", () => { + const facade = TestBed.inject(MemberStatisticsFacade); + facade.load(); + TestBed.resetTestingModule(); + expect(response.observed).toBe(false); + response.next(ok(data)); + expect(facade.state().status).toBe("loading"); + }); + + it.each(["network", "HTTP 500 private raw body"])( + "use-case скрывает техническую ошибку %s", + message => { + TestBed.overrideProvider(GetMemberStatisticsUseCase, { + useFactory: () => new GetMemberStatisticsUseCase(), + }); + TestBed.configureTestingModule({ + providers: [ + { + provide: MemberRepositoryPort, + useValue: { getStatistics: () => throwError(() => new Error(message)) }, + }, + ], + }); + const facade = TestBed.inject(MemberStatisticsFacade); + facade.load(); + expect(facade.state()).toMatchObject({ status: "failure", error: "member_statistics_error" }); + expect(JSON.stringify(facade.state())).not.toContain(message); + }, + ); +}); diff --git a/projects/social_platform/src/app/api/member/facades/member-statistics.facade.ts b/projects/social_platform/src/app/api/member/facades/member-statistics.facade.ts new file mode 100644 index 000000000..df3e9c13c --- /dev/null +++ b/projects/social_platform/src/app/api/member/facades/member-statistics.facade.ts @@ -0,0 +1,29 @@ +/** @format */ + +import { DestroyRef, inject, Injectable, signal } from "@angular/core"; +import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { AsyncState, failure, loading, success } from "@domain/shared/async-state"; +import { MemberStatistics } from "@domain/member/member-statistics.model"; +import { GetMemberStatisticsUseCase } from "../use-cases/get-member-statistics.use-case"; + +/** Состояние статистики живёт вместе со страницей, отдельно от поисковой выдачи. */ +@Injectable() +export class MemberStatisticsFacade { + private readonly getStatistics = inject(GetMemberStatisticsUseCase); + private readonly destroyRef = inject(DestroyRef); + private readonly current = signal>(loading()); + readonly state = this.current.asReadonly(); + private started = false; + + /** Один запрос на открытие страницы; фильтры и смена layout не запускают его повторно. */ + load(): void { + if (this.started) return; + this.started = true; + this.getStatistics + .execute() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(result => { + this.current.set(result.ok ? success(result.value) : failure(result.error)); + }); + } +} diff --git a/projects/social_platform/src/app/api/member/use-cases/get-member-statistics.use-case.ts b/projects/social_platform/src/app/api/member/use-cases/get-member-statistics.use-case.ts new file mode 100644 index 000000000..59ef87715 --- /dev/null +++ b/projects/social_platform/src/app/api/member/use-cases/get-member-statistics.use-case.ts @@ -0,0 +1,21 @@ +/** @format */ + +import { inject, Injectable } from "@angular/core"; +import { catchError, map, Observable, of } from "rxjs"; +import { MemberStatistics } from "@domain/member/member-statistics.model"; +import { MemberRepositoryPort } from "@domain/member/ports/member.repository.port"; +import { fail, ok, Result } from "@domain/shared/result.type"; + +/** Сценарий чтения четырёх агрегатов с безопасной ошибкой для UI. */ +@Injectable({ providedIn: "root" }) +export class GetMemberStatisticsUseCase { + private readonly repository = inject(MemberRepositoryPort); + + /** Ошибка запроса/контракта остаётся ошибкой, а не набором нулей. */ + execute(): Observable> { + return this.repository.getStatistics().pipe( + map(data => ok(data)), + catchError(() => of(fail("member_statistics_error" as const))), + ); + } +} diff --git a/projects/social_platform/src/app/domain/member/member-statistics.model.ts b/projects/social_platform/src/app/domain/member/member-statistics.model.ts new file mode 100644 index 000000000..32c18600c --- /dev/null +++ b/projects/social_platform/src/app/domain/member/member-statistics.model.ts @@ -0,0 +1,12 @@ +/** + * Глобальные счётчики активных участников, независимые от выдачи каталога. + * + * @format + */ + +export interface MemberStatistics { + readonly total: number; + readonly inProjects: number; + readonly inPrograms: number; + readonly newLast30Days: number; +} diff --git a/projects/social_platform/src/app/domain/member/ports/member.repository.port.ts b/projects/social_platform/src/app/domain/member/ports/member.repository.port.ts index c833cf54a..61446d59a 100644 --- a/projects/social_platform/src/app/domain/member/ports/member.repository.port.ts +++ b/projects/social_platform/src/app/domain/member/ports/member.repository.port.ts @@ -3,9 +3,13 @@ import { Observable } from "rxjs"; import { ApiPagination } from "../../other/api-pagination.model"; import { User } from "../../auth/user.model"; +import { MemberStatistics } from "../member-statistics.model"; -/** Порт репозитория участников: список members (skip/take/фильтры) и mentors. */ +/** Порт каталога: фильтруемый список и отдельная глобальная статистика участников. */ export abstract class MemberRepositoryPort { + /** Получает агрегаты без параметров поиска и пагинации. */ + abstract getStatistics(): Observable; + abstract getMembers( skip: number, take: number, diff --git a/projects/social_platform/src/app/infrastructure/adapters/member/member-http.adapter.ts b/projects/social_platform/src/app/infrastructure/adapters/member/member-http.adapter.ts index 1e4de3ccc..e108f541d 100644 --- a/projects/social_platform/src/app/infrastructure/adapters/member/member-http.adapter.ts +++ b/projects/social_platform/src/app/infrastructure/adapters/member/member-http.adapter.ts @@ -6,6 +6,7 @@ import { Observable } from "rxjs"; import { HttpParams } from "@angular/common/http"; import { ApiPagination } from "@domain/other/api-pagination.model"; import { User } from "@domain/auth/user.model"; +import { MemberStatistics } from "@domain/member/member-statistics.model"; /** HTTP-адаптер участников: `/auth/public-users` (фильтр user_type разделяет members/mentors). */ @Injectable({ providedIn: "root" }) @@ -13,6 +14,11 @@ export class MemberHttpAdapter { private readonly AUTH_PUBLIC_USERS_URL = "/auth/public-users"; private readonly apiService = inject(ApiService); + /** Отдельный GET без фильтров; snake_case преобразует общий CamelcaseInterceptor. */ + getStatistics(): Observable { + return this.apiService.get(`${this.AUTH_PUBLIC_USERS_URL}/stats/`); + } + getMembers( skip: number, take: number, diff --git a/projects/social_platform/src/app/infrastructure/adapters/member/member-statistics-contract.spec.ts b/projects/social_platform/src/app/infrastructure/adapters/member/member-statistics-contract.spec.ts new file mode 100644 index 000000000..392857400 --- /dev/null +++ b/projects/social_platform/src/app/infrastructure/adapters/member/member-statistics-contract.spec.ts @@ -0,0 +1,67 @@ +/** @format */ + +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from "@angular/common/http"; +import { HttpTestingController, provideHttpClientTesting } from "@angular/common/http/testing"; +import { TestBed } from "@angular/core/testing"; +import { firstValueFrom } from "rxjs"; +import { CamelcaseInterceptor } from "@core/lib/interceptors/camelcase.interceptor"; +import { API_URL } from "@core/lib/providers"; +import { LoggerService } from "@core/lib/services/logger/logger.service"; +import { MemberRepositoryPort } from "@domain/member/ports/member.repository.port"; +import { MemberRepository } from "../../repository/member/member.repository"; +import { GetMemberStatisticsUseCase } from "@api/member/use-cases/get-member-statistics.use-case"; + +describe("Контракт глобальной статистики участников", () => { + beforeEach(() => + TestBed.configureTestingModule({ + providers: [ + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + { provide: API_URL, useValue: "https://local.test" }, + { provide: HTTP_INTERCEPTORS, useClass: CamelcaseInterceptor, multi: true }, + { provide: LoggerService, useValue: { warn: vi.fn() } }, + { provide: MemberRepositoryPort, useExisting: MemberRepository }, + ], + }), + ); + afterEach(() => TestBed.inject(HttpTestingController).verify()); + + function request() { + const result = firstValueFrom(TestBed.inject(GetMemberStatisticsUseCase).execute()); + const http = TestBed.inject(HttpTestingController).expectOne( + "https://local.test/auth/public-users/stats/", + ); + expect(http.request.method).toBe("GET"); + expect(http.request.params.keys()).toEqual([]); + return { result, http }; + } + + it("передаёт четыре snake_case агрегата через interceptor, репозиторий и use-case без фильтров списка", async () => { + const { result, http } = request(); + http.flush({ total: 1248, in_projects: 684, in_programs: 214, new_last_30_days: 63 }); + expect(await result).toEqual({ + ok: true, + value: { total: 1248, inProjects: 684, inPrograms: 214, newLast30Days: 63 }, + }); + }); + + it("сохраняет реальные нули", async () => { + const { result, http } = request(); + http.flush({ total: 0, in_projects: 0, in_programs: 0, new_last_30_days: 0 }); + expect(await result).toEqual({ + ok: true, + value: { total: 0, inProjects: 0, inPrograms: 0, newLast30Days: 0 }, + }); + }); + + it.each([ + { total: 4 }, + { total: 4, in_projects: -1, in_programs: 0, new_last_30_days: 0 }, + { total: 4, in_projects: 1.5, in_programs: 0, new_last_30_days: 0 }, + { total: "4", in_projects: 0, in_programs: 0, new_last_30_days: 0 }, + ])("не подменяет неверный контракт нулями: %j", async payload => { + const { result, http } = request(); + http.flush(payload); + expect(await result).toEqual({ ok: false, error: "member_statistics_error" }); + }); +}); diff --git a/projects/social_platform/src/app/infrastructure/repository/member/member.repository.ts b/projects/social_platform/src/app/infrastructure/repository/member/member.repository.ts index bb78a3717..4cdc72ed5 100644 --- a/projects/social_platform/src/app/infrastructure/repository/member/member.repository.ts +++ b/projects/social_platform/src/app/infrastructure/repository/member/member.repository.ts @@ -7,12 +7,26 @@ import { ApiPagination } from "@domain/other/api-pagination.model"; import { MemberRepositoryPort } from "@domain/member/ports/member.repository.port"; import { MemberHttpAdapter } from "../../adapters/member/member-http.adapter"; import { userFromRaw } from "@utils/userRaw"; +import { MemberStatistics } from "@domain/member/member-statistics.model"; -/** Репозиторий участников: passthrough + `plainToInstance(User)`. */ +/** Преобразует участников в domain-модель и проверяет контракт глобальных счётчиков. */ @Injectable({ providedIn: "root" }) export class MemberRepository implements MemberRepositoryPort { private readonly memberAdapter = inject(MemberHttpAdapter); + /** Не превращает неполный или некорректный контракт в ложные нулевые показатели. */ + getStatistics(): Observable { + return this.memberAdapter.getStatistics().pipe( + map(data => { + const values = [data?.total, data?.inProjects, data?.inPrograms, data?.newLast30Days]; + if (values.some(value => !Number.isSafeInteger(value) || value < 0)) { + throw new Error("Некорректный ответ статистики участников"); + } + return data; + }), + ); + } + getMembers( skip: number, take: number, diff --git a/projects/social_platform/src/app/ui/pages/members/member-statistics-card/member-statistics-card.component.html b/projects/social_platform/src/app/ui/pages/members/member-statistics-card/member-statistics-card.component.html new file mode 100644 index 000000000..4e728a2c9 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/members/member-statistics-card/member-statistics-card.component.html @@ -0,0 +1,21 @@ + + +
+

Статистика

+
+ @for (row of rows(); track row.key) { +
+ +
{{ row.label }}
+
{{ row.value }}
+
+ } +
+ {{ status() }} +
diff --git a/projects/social_platform/src/app/ui/pages/members/member-statistics-card/member-statistics-card.component.scss b/projects/social_platform/src/app/ui/pages/members/member-statistics-card/member-statistics-card.component.scss new file mode 100644 index 000000000..f720f7def --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/members/member-statistics-card/member-statistics-card.component.scss @@ -0,0 +1,130 @@ +/** @format */ + +@use "styles/responsive"; + +:host { + box-sizing: border-box; + display: block; + width: 100%; + min-width: 0; + max-width: 157px; +} + +.statistics { + box-sizing: border-box; + width: 100%; + min-width: 0; + max-width: 157px; + padding: 10px; + background: var(--light-white); + border: 1px solid var(--medium-grey-for-outline); + border-radius: var(--rounded-xl); + + &__title { + margin: 0 0 6px; + font-size: 14px; + font-weight: 600; + line-height: 20px; + } + + &__metrics { + margin: 0; + } + + &__row { + display: grid; + grid-template-columns: 22px minmax(0, 1fr) auto; + gap: 4px; + align-items: center; + min-width: 0; + min-height: 58px; + } + + &__row + &__row { + border-top: 1px solid var(--medium-grey-for-outline); + } + + &__icon { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + color: var(--accent-dark); + background: var(--accent-light); + border-radius: var(--rounded-lg); + } + + &__label { + min-width: 0; + font-size: 9px; + line-height: 13px; + color: var(--grey-for-text); + overflow-wrap: anywhere; + } + + &__value { + min-width: 0; + margin: 0; + font-size: 13px; + font-weight: 600; + font-variant-numeric: tabular-nums; + line-height: 18px; + color: var(--accent-dark); + text-align: right; + white-space: nowrap; + } + + &__status { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + } +} + +@media (max-width: #{responsive.$desktop - 1px}) { + :host, + .statistics { + max-width: none; + } + + .statistics { + &__metrics { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px 12px; + } + + &__row { + grid-template-columns: 22px minmax(0, 1fr); + row-gap: 2px; + column-gap: 6px; + padding: 6px 0; + border: 0; + } + + &__row + &__row { + border-top: 0; + } + + &__icon { + grid-row: 1 / 3; + } + + &__value { + grid-row: 1; + grid-column: 2; + text-align: left; + } + + &__label { + grid-row: 2; + grid-column: 2; + font-size: 10px; + } + } +} diff --git a/projects/social_platform/src/app/ui/pages/members/member-statistics-card/member-statistics-card.component.spec.ts b/projects/social_platform/src/app/ui/pages/members/member-statistics-card/member-statistics-card.component.spec.ts new file mode 100644 index 000000000..67fd4897a --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/members/member-statistics-card/member-statistics-card.component.spec.ts @@ -0,0 +1,67 @@ +/** @format */ + +import { TestBed } from "@angular/core/testing"; +import { failure, loading, success } from "@domain/shared/async-state"; +import { MemberStatisticsCardComponent } from "./member-statistics-card.component"; + +describe("Компактная статистика участников", () => { + function card(state: ReturnType | ReturnType) { + const fixture = TestBed.createComponent(MemberStatisticsCardComponent); + fixture.componentRef.setInput("state", state); + fixture.detectChanges(); + const root: HTMLElement = fixture.nativeElement; + return { + fixture, + root, + values: () => Array.from(root.querySelectorAll("dd"), item => item.textContent!.trim()), + }; + } + + it.each([loading(), failure("raw secret HTTP body")])( + "показывает прочерки и безопасное доступное состояние: $status", + state => { + const { root, values } = card(state); + expect(values()).toEqual(["—", "—", "—", "—"]); + expect(root.textContent).not.toContain("raw secret"); + expect(root.querySelector('[role="status"]')!.textContent).toContain( + state.status === "failure" ? "Не удалось загрузить" : "Загрузка", + ); + expect(root.querySelector("section")!.getAttribute("aria-busy")).toBe( + String(state.status === "loading"), + ); + }, + ); + + it.each([0, 9, 63, 999, 1248, 12345, 12500, 999999])( + "выводит четыре реальных значения %i, без процентов и лишних действий", + value => { + const { root, values } = card( + success({ total: value, inProjects: value, inPrograms: value, newLast30Days: value }), + ); + expect(values()).toEqual(Array(4).fill(new Intl.NumberFormat("ru-RU").format(value))); + expect(Array.from(root.querySelectorAll("dt"), item => item.textContent!.trim())).toEqual([ + "Всего участников", + "В проектах", + "В программах", + "Новых за 30 дней", + ]); + expect(root.querySelectorAll("button, a")).toHaveLength(0); + expect(root.textContent).not.toMatch(/скоро|Подробнее|эффективность|%/); + }, + ); + + it("обновляет данные в той же структуре карточки после loading и ошибки", () => { + const { fixture, root, values } = card(loading()); + const section = root.querySelector("section"); + fixture.componentRef.setInput( + "state", + success({ total: 0, inProjects: 9, inPrograms: 63, newLast30Days: 999 }), + ); + fixture.detectChanges(); + expect(values()).toEqual(["0", "9", "63", "999"]); + fixture.componentRef.setInput("state", failure("hidden")); + fixture.detectChanges(); + expect(values()).toEqual(["—", "—", "—", "—"]); + expect(root.querySelector("section")).toBe(section); + }); +}); diff --git a/projects/social_platform/src/app/ui/pages/members/member-statistics-card/member-statistics-card.component.ts b/projects/social_platform/src/app/ui/pages/members/member-statistics-card/member-statistics-card.component.ts new file mode 100644 index 000000000..a426ded16 --- /dev/null +++ b/projects/social_platform/src/app/ui/pages/members/member-statistics-card/member-statistics-card.component.ts @@ -0,0 +1,64 @@ +/** @format */ + +import { ChangeDetectionStrategy, Component, computed, input } from "@angular/core"; +import { MemberStatistics } from "@domain/member/member-statistics.model"; +import { AsyncState } from "@domain/shared/async-state"; +import { IconComponent } from "@ui/primitives/icon/icon.component"; + +/** Четыре глобальные метрики: одна разметка для узкого sidebar и мобильной сетки. */ +@Component({ + selector: "app-member-statistics-card", + imports: [IconComponent], + templateUrl: "./member-statistics-card.component.html", + styleUrl: "./member-statistics-card.component.scss", + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class MemberStatisticsCardComponent { + readonly state = input.required>(); + private readonly formatter = new Intl.NumberFormat("ru-RU"); + protected readonly rows = computed(() => { + const state = this.state(); + const value = (key: keyof MemberStatistics) => + state.status === "success" ? this.formatter.format(state.data[key]) : "—"; + return [ + { + key: "total", + label: "Всего участников", + icon: "people", + viewBox: "0 0 24 24", + value: value("total"), + }, + { + key: "projects", + label: "В проектах", + icon: "projects", + viewBox: "0 0 10 11", + value: value("inProjects"), + }, + { + key: "programs", + label: "В программах", + icon: "academic-hat", + viewBox: "0 0 28 15", + value: value("inPrograms"), + }, + { + key: "new", + label: "Новых за 30 дней", + icon: "graph", + viewBox: "0 0 12 12", + value: value("newLast30Days"), + }, + ]; + }); + protected readonly status = computed(() => { + switch (this.state().status) { + case "success": + return "Статистика участников загружена"; + case "failure": + return "Не удалось загрузить статистику участников"; + default: + return "Загрузка статистики участников"; + } + }); +} diff --git a/projects/social_platform/src/app/ui/pages/members/members-mobile.spec.ts b/projects/social_platform/src/app/ui/pages/members/members-mobile.spec.ts index 07e8d1020..69ac9ef0f 100644 --- a/projects/social_platform/src/app/ui/pages/members/members-mobile.spec.ts +++ b/projects/social_platform/src/app/ui/pages/members/members-mobile.spec.ts @@ -6,7 +6,10 @@ import { provideNoopAnimations } from "@angular/platform-browser/animations"; import { provideRouter, Router } from "@angular/router"; import { BreakpointObserver } from "@angular/cdk/layout"; import { OverlayContainer } from "@angular/cdk/overlay"; -import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { BehaviorSubject, firstValueFrom, of } from "rxjs"; +import { MemberStatisticsFacade } from "@api/member/facades/member-statistics.facade"; +import { GetMemberStatisticsUseCase } from "@api/member/use-cases/get-member-statistics.use-case"; +import { ok } from "@domain/shared/result.type"; import { MembersInfoService } from "@api/member/facades/members-info.service"; import { MembersUIInfoService } from "@api/member/facades/ui/members-ui-info.service"; import { ProfileDetailUIInfoService } from "@api/profile/facades/detail/ui/profile-detail-ui-info.service"; @@ -20,12 +23,16 @@ import { MembersFiltersComponent } from "./members-filters/members-filters.compo describe("Мобильное представление участников", () => { const layout = new BehaviorSubject({ matches: true, breakpoints: {} }); const redirect = vi.fn(); + const statistics = { total: 1248, inProjects: 684, inPrograms: 214, newLast30Days: 63 }; + const loadStatistics = vi.fn(); beforeEach(async () => { layout.next({ matches: true, breakpoints: {} }); redirect.mockReset(); + loadStatistics.mockReset().mockReturnValue(of(ok(statistics))); await TestBed.configureTestingModule({ imports: [MembersComponent], providers: [ + { provide: GetMemberStatisticsUseCase, useValue: { execute: loadStatistics } }, provideRouter([]), provideNoopAnimations(), { provide: BreakpointObserver, useValue: { observe: () => layout, isMatched: () => true } }, @@ -43,6 +50,7 @@ describe("Мобильное представление участников", ( .overrideComponent(MembersComponent, { set: { providers: [ + MemberStatisticsFacade, MembersUIInfoService, { provide: MembersInfoService, @@ -89,6 +97,34 @@ describe("Мобильное представление участников", ( return primitive; } + it("держит статистику перед мобильным списком, под desktop-фильтрами и независимо от поиска и пагинации", async () => { + const f = await page(); + const ui = f.debugElement.injector.get(MembersUIInfoService); + const card = f.nativeElement.querySelector("app-member-statistics-card") as HTMLElement; + const list = f.nativeElement.querySelector(".page__list") as HTMLElement; + expect(card.compareDocumentPosition(list) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + expect(card.textContent).toContain("1 248"); + expect(f.nativeElement.querySelector("app-soon-card")).toBeNull(); + ui.searchForm.patchValue({ search: "Иван" }); + ui.filterForm.patchValue({ keySkill: "Java", speciality: "Разработчик" }); + ui.applyMembersPagination({ count: 1, results: [], next: "?offset=20", previous: "" }); + await f.whenStable(); + expect(ui.membersTotalCount()).toBe(1); + expect(card.textContent).toContain("1 248"); + ui.applyMembersPagination({ count: 2, results: [], next: "", previous: "?offset=0" }); + layout.next({ matches: false, breakpoints: {} }); + await f.whenStable(); + const sidebar = f.nativeElement.querySelector(".page__left") as HTMLElement; + const filters = sidebar.querySelector("app-members-filters")!; + const desktopCard = sidebar.querySelector("app-member-statistics-card")!; + expect( + filters.compareDocumentPosition(desktopCard) & Node.DOCUMENT_POSITION_FOLLOWING, + ).toBeTruthy(); + expect(desktopCard.textContent).toContain("1 248"); + expect(loadStatistics).toHaveBeenCalledExactlyOnceWith(); + await finish(f); + }); + it("использует прежний search control и переход в профиль, не держит фильтры рядом со списком", async () => { const f = await page(); const ui = f.debugElement.injector.get(MembersUIInfoService); diff --git a/projects/social_platform/src/app/ui/pages/members/members.component.html b/projects/social_platform/src/app/ui/pages/members/members.component.html index d16afe0fb..201dad06f 100644 --- a/projects/social_platform/src/app/ui/pages/members/members.component.html +++ b/projects/social_platform/src/app/ui/pages/members/members.component.html @@ -28,8 +28,8 @@ Перейти в профиль - + } @else {
@@ -70,7 +70,7 @@
- +
@if (filtersOpen() && compactLayout()) { diff --git a/projects/social_platform/src/app/ui/pages/members/members.component.scss b/projects/social_platform/src/app/ui/pages/members/members.component.scss index 2fbc8d98c..909e5a4c5 100644 --- a/projects/social_platform/src/app/ui/pages/members/members.component.scss +++ b/projects/social_platform/src/app/ui/pages/members/members.component.scss @@ -1,3 +1,5 @@ +/** @format */ + @use "styles/responsive"; .page { @@ -125,8 +127,9 @@ } &__statistics { - width: 157px; - margin: 4px auto 0; + width: 100%; + min-width: 0; + margin: 4px 0 0; } } } diff --git a/projects/social_platform/src/app/ui/pages/members/members.component.spec.ts b/projects/social_platform/src/app/ui/pages/members/members.component.spec.ts index 79776770d..a07b84cbf 100644 --- a/projects/social_platform/src/app/ui/pages/members/members.component.spec.ts +++ b/projects/social_platform/src/app/ui/pages/members/members.component.spec.ts @@ -13,6 +13,8 @@ import { initial } from "@domain/shared/async-state"; import { FormBuilder } from "@angular/forms"; import { SkillsRepositoryPort } from "@domain/skills/ports/skills.repository.port"; import { SpecializationsRepositoryPort } from "@domain/specializations/ports/specializations.repository.port"; +import { GetMemberStatisticsUseCase } from "@api/member/use-cases/get-member-statistics.use-case"; +import { ok } from "@domain/shared/result.type"; describe("MembersComponent", () => { let component: MembersComponent; @@ -52,6 +54,12 @@ describe("MembersComponent", () => { await TestBed.configureTestingModule({ imports: [MembersComponent], providers: [ + { + provide: GetMemberStatisticsUseCase, + useValue: { + execute: () => of(ok({ total: 0, inProjects: 0, inPrograms: 0, newLast30Days: 0 })), + }, + }, { provide: AuthRepositoryPort, useValue: authPortSpy }, { provide: SkillsRepositoryPort, diff --git a/projects/social_platform/src/app/ui/pages/members/members.component.ts b/projects/social_platform/src/app/ui/pages/members/members.component.ts index a81a3ec41..9c79340e5 100644 --- a/projects/social_platform/src/app/ui/pages/members/members.component.ts +++ b/projects/social_platform/src/app/ui/pages/members/members.component.ts @@ -22,7 +22,8 @@ import { MembersFiltersComponent } from "./members-filters/members-filters.compo import { MemberCardComponent } from "./member-card/member-card.component"; import { BackComponent } from "@uilib"; import { ButtonComponent } from "@ui/primitives"; -import { SoonCardComponent } from "@ui/primitives/soon-card/soon-card.component"; +import { MemberStatisticsCardComponent } from "./member-statistics-card/member-statistics-card.component"; +import { MemberStatisticsFacade } from "@api/member/facades/member-statistics.facade"; import { MembersInfoService } from "@api/member/facades/members-info.service"; import { MembersUIInfoService } from "@api/member/facades/ui/members-ui-info.service"; import { AppRoutes } from "@api/paths/app-routes"; @@ -43,16 +44,23 @@ import { MemberFiltersDialogComponent } from "./member-filters-dialog/member-fil MemberCardComponent, BackComponent, ButtonComponent, - SoonCardComponent, + MemberStatisticsCardComponent, MemberFiltersDialogComponent, ], - providers: [MembersInfoService, MembersUIInfoService, ProfileDetailUIInfoService], + providers: [ + MembersInfoService, + MembersUIInfoService, + ProfileDetailUIInfoService, + MemberStatisticsFacade, + ], }) export class MembersComponent implements OnInit, AfterViewInit { readonly membersRoot = viewChild | undefined>("membersRoot"); // Ссылка на корневой элемент списка private readonly membersInfoService = inject(MembersInfoService); private readonly membersUIInfoService = inject(MembersUIInfoService); + private readonly statisticsFacade = inject(MemberStatisticsFacade); + protected readonly statisticsState = this.statisticsFacade.state; protected readonly members = this.membersUIInfoService.members; @@ -86,6 +94,7 @@ export class MembersComponent implements OnInit, AfterViewInit { ngOnInit(): void { this.membersInfoService.initializationMembers(); + this.statisticsFacade.load(); } ngAfterViewInit(): void {