@danikokonn/yarik-frontend-lib 2.0.58 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +309 -1
- package/dist/components/RichFilterTextField/RichFilterTextField.js +1 -1
- package/dist/components/SubtaskItem/SubtaskItem.d.ts.map +1 -1
- package/dist/components/SubtaskItem/SubtaskItem.js +11 -12
- package/dist/components/TaskLoader/TaskLoader.js +2 -2
- package/dist/providers/AirflowProvider.d.ts +7 -6
- package/dist/providers/AirflowProvider.d.ts.map +1 -1
- package/dist/providers/AirflowProvider.js +43 -40
- package/dist/providers/DagStateProvider.d.ts +3 -3
- package/dist/providers/DagStateProvider.d.ts.map +1 -1
- package/dist/providers/DagStateProvider.js +33 -37
- package/dist/providers/SessionProvider.d.ts +1 -3
- package/dist/providers/SessionProvider.d.ts.map +1 -1
- package/dist/providers/SessionProvider.js +9 -9
- package/dist/providers/SnackBarProvider.d.ts +3 -2
- package/dist/providers/SnackBarProvider.d.ts.map +1 -1
- package/dist/providers/SnackBarProvider.js +20 -13
- package/package.json +26 -18
package/README.md
CHANGED
|
@@ -1 +1,309 @@
|
|
|
1
|
-
# YARIK-frontend-lib
|
|
1
|
+
# YARIK-frontend-lib
|
|
2
|
+
|
|
3
|
+
Библиотека переиспользуемых React-компонентов, утилит и провайдеров.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@danikokonn/yarik-frontend-lib)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Содержание
|
|
11
|
+
|
|
12
|
+
- [Установка](#установка)
|
|
13
|
+
- [Требования](#требования)
|
|
14
|
+
- [Использование](#использование)
|
|
15
|
+
- [Компоненты](#компоненты)
|
|
16
|
+
- [Провайдеры](#провайдеры)
|
|
17
|
+
- [Утилиты и HTTP-клиент](#утилиты-и-http-клиент)
|
|
18
|
+
- [Разработка](#разработка)
|
|
19
|
+
- [Тестирование](#тестирование)
|
|
20
|
+
- [Публикация](#публикация)
|
|
21
|
+
- [Как дополнять README](#как-дополнять-readme)
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Установка
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @danikokonn/yarik-frontend-lib
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Требования
|
|
34
|
+
|
|
35
|
+
| Зависимость | Версия |
|
|
36
|
+
|-------------|----------|
|
|
37
|
+
| React | >=18 <20 |
|
|
38
|
+
| react-dom | >=18 <20 |
|
|
39
|
+
|
|
40
|
+
MUI, Emotion и остальные зависимости устанавливаются автоматически.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Использование
|
|
45
|
+
|
|
46
|
+
### Импорт компонентов
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { SmartTable, Navigation, PageWrapper } from '@danikokonn/yarik-frontend-lib';
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Импорт утилит
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { prettyDatetime, getCSRFToken } from '@danikokonn/yarik-frontend-lib/utils';
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Импорт HTTP-клиента
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
import { get, post } from '@danikokonn/yarik-frontend-lib/http';
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Импорт типов
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import type { Dag, DagRun, TaskInstance } from '@danikokonn/yarik-frontend-lib/types';
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Подключение провайдеров
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { SnackBarProvider, SessionProvider } from '@danikokonn/yarik-frontend-lib';
|
|
74
|
+
|
|
75
|
+
function App() {
|
|
76
|
+
return (
|
|
77
|
+
<SessionProvider>
|
|
78
|
+
<SnackBarProvider>
|
|
79
|
+
{/* ваше приложение */}
|
|
80
|
+
</SnackBarProvider>
|
|
81
|
+
</SessionProvider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Компоненты
|
|
89
|
+
|
|
90
|
+
| Компонент | Описание |
|
|
91
|
+
|----------------------------|------------------------------------------------------------------|
|
|
92
|
+
| `ActionDialog` | Диалог подтверждения действия с опциональной формой ввода |
|
|
93
|
+
| `ColumnSearchInput` | Поле поиска для столбца таблицы |
|
|
94
|
+
| `DateTimeRangePicker` | Выбор диапазона дат и времени |
|
|
95
|
+
| `DatetimeRangeInput` | Ввод диапазона дата/время |
|
|
96
|
+
| `Footer` | Нижний колонтитул страницы |
|
|
97
|
+
| `IpAddressInput` | Поле ввода IP-адреса с валидацией |
|
|
98
|
+
| `IpAddressSwitchableInput` | Переключаемое поле ввода IP-адреса |
|
|
99
|
+
| `LocalizedDatetimePicker` | Выбор даты/времени с поддержкой часовых поясов |
|
|
100
|
+
| `Navigation` | Навигационная панель с поддержкой меню |
|
|
101
|
+
| `NumberRangeInput` | Ввод числового диапазона |
|
|
102
|
+
| `PageWrapper` | Обёртка страницы (Navigation + контент + Footer) |
|
|
103
|
+
| `RichFilterTextField` | Расширенное поле фильтрации с историей и подсказками |
|
|
104
|
+
| `SmartTable` | Виртуализированная таблица с сортировкой, пагинацией, фильтрами |
|
|
105
|
+
| `SortBtn` | Кнопка сортировки |
|
|
106
|
+
| `SubtaskItem` | Отображение элемента подзадачи |
|
|
107
|
+
| `TaskLoader` | Компонент состояния загрузки |
|
|
108
|
+
| `TaskStateDialog` | Диалог отображения состояния задачи |
|
|
109
|
+
|
|
110
|
+
### Иконки
|
|
111
|
+
|
|
112
|
+
Доступны 9 SVG-иконок: `GenerateAndRunIcon`, `GenerateIcon`, `IntegerIcon`, `ProcessingIcon`, `QueueIcon`, `RangeIcon`, `RunIcon`, `TextModeIcon`, `TreeModeIcon`.
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { RunIcon } from '@danikokonn/yarik-frontend-lib/components/icons/RunIcon';
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Провайдеры
|
|
121
|
+
|
|
122
|
+
### SnackBarProvider
|
|
123
|
+
|
|
124
|
+
Контекст уведомлений на основе notistack.
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { SnackBarProvider, useSnackbarContext } from '@danikokonn/yarik-frontend-lib';
|
|
128
|
+
|
|
129
|
+
function MyComponent() {
|
|
130
|
+
const { enqueueSnackbar } = useSnackbarContext();
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<button onClick={() => enqueueSnackbar('Успешно!', { variant: 'success' })}>
|
|
134
|
+
Показать уведомление
|
|
135
|
+
</button>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Доступные варианты: `default`, `error`, `success`, `warning`, `info`.
|
|
141
|
+
|
|
142
|
+
### SessionProvider
|
|
143
|
+
|
|
144
|
+
Управление состоянием сессии и аутентификации.
|
|
145
|
+
|
|
146
|
+
### AirflowProvider
|
|
147
|
+
|
|
148
|
+
Контекст интеграции с Apache Airflow.
|
|
149
|
+
|
|
150
|
+
### DagStateProvider
|
|
151
|
+
|
|
152
|
+
Управление состоянием DAG-рабочих процессов.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Утилиты и HTTP-клиент
|
|
157
|
+
|
|
158
|
+
### Утилиты (`/utils`)
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import {
|
|
162
|
+
prettyDatetime, // форматирование даты/времени для отображения
|
|
163
|
+
formatDate, // форматирование даты
|
|
164
|
+
formatDuration, // форматирование длительности
|
|
165
|
+
getCSRFToken, // получение CSRF-токена
|
|
166
|
+
setCSRFToken, // установка CSRF-токена
|
|
167
|
+
useDebounce, // хук debounce
|
|
168
|
+
gettextTS, // i18n: перевод строки
|
|
169
|
+
ngettextTS, // i18n: перевод с учётом числа
|
|
170
|
+
} from '@danikokonn/yarik-frontend-lib/utils';
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### HTTP-клиент (`/http`)
|
|
174
|
+
|
|
175
|
+
Fetch-обёртка с поддержкой CSRF и автоматическим определением истечения сессии (401).
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
import { get, post } from '@danikokonn/yarik-frontend-lib/http';
|
|
179
|
+
|
|
180
|
+
const data = await get('/api/endpoint');
|
|
181
|
+
await post('/api/endpoint', { key: 'value' });
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Разработка
|
|
187
|
+
|
|
188
|
+
### Установка зависимостей
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npm install
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Сборка
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
npm run build
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Компилирует TypeScript в `dist/`. Декларации типов генерируются автоматически.
|
|
201
|
+
|
|
202
|
+
### Добавление нового компонента
|
|
203
|
+
|
|
204
|
+
Создайте папку `src/components/<ComponentName>/` со структурой:
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
src/components/MyComponent/
|
|
208
|
+
├── MyComponent.tsx # компонент
|
|
209
|
+
├── MyComponentProps.ts # интерфейс пропсов
|
|
210
|
+
├── MyComponent.test.tsx # тесты (обязательно, см. раздел «Тестирование»)
|
|
211
|
+
└── index.ts # export { default } from './MyComponent'
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Добавьте экспорт в `src/components/index.ts`.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Тестирование
|
|
219
|
+
|
|
220
|
+
Проект использует [Vitest](https://vitest.dev/) + [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/).
|
|
221
|
+
|
|
222
|
+
### Запуск тестов
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
npm test # watch-режим (разработка)
|
|
226
|
+
npm run test:run # однократный запуск (CI)
|
|
227
|
+
npm run coverage # запуск с отчётом о покрытии
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Структура тестов
|
|
231
|
+
|
|
232
|
+
Тестовые файлы располагаются рядом с компонентом (`*.test.tsx`) и автоматически подхватываются Vitest.
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
src/components/SortBtn/
|
|
236
|
+
├── SortBtn.tsx
|
|
237
|
+
├── SortBtnProps.ts
|
|
238
|
+
├── SortBtn.test.tsx ← тесты
|
|
239
|
+
└── index.ts
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Обязательный плейсхолдер
|
|
243
|
+
|
|
244
|
+
При создании нового компонента **всегда** создавайте файл `<ComponentName>.test.tsx` рядом с компонентом — даже если тесты пока не написаны:
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
import { render } from '@testing-library/react'
|
|
248
|
+
import MyComponent from './MyComponent'
|
|
249
|
+
|
|
250
|
+
describe('MyComponent', () => {
|
|
251
|
+
/**
|
|
252
|
+
* Проверяем, что компонент рендерится без ошибок с обязательными пропсами.
|
|
253
|
+
* Базовая страховка от синтаксических и runtime-ошибок при первом рендере.
|
|
254
|
+
*/
|
|
255
|
+
it.todo('renders correctly')
|
|
256
|
+
})
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Это сигнализирует о том, что компонент ещё не покрыт тестами, и не даёт файлу «потеряться».
|
|
260
|
+
|
|
261
|
+
### Описание тест-кейсов
|
|
262
|
+
|
|
263
|
+
Каждый блок `it` (в том числе `it.todo`) должен предваряться JSDoc-комментарием, объясняющим **что** тестируется и **зачем**. Названия теста недостаточно — комментарий должен отвечать на вопрос «что сломается в продакшене, если этот тест упадёт»:
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
/**
|
|
267
|
+
* Проверяем, что первый клик из состояния "none" вызывает onToggleSort с направлением "asc".
|
|
268
|
+
* Это начало трёхступенчатого цикла: none → asc → desc → none.
|
|
269
|
+
* Callback должен получать и поле, и новое направление, чтобы родитель мог обновить свой стейт.
|
|
270
|
+
*/
|
|
271
|
+
it('calls onToggleSort with asc when order is none', async () => {
|
|
272
|
+
// ...
|
|
273
|
+
})
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Правило распространяется на все тесты — и реальные, и заглушки `it.todo`.
|
|
277
|
+
|
|
278
|
+
### Конфигурация
|
|
279
|
+
|
|
280
|
+
| Файл | Назначение |
|
|
281
|
+
|------|------------|
|
|
282
|
+
| `vitest.config.ts` | Основная конфигурация Vitest |
|
|
283
|
+
| `src/test/setup.ts` | Подключение `@testing-library/jest-dom` |
|
|
284
|
+
| `tsconfig.test.json` | TypeScript-конфигурация для тестов |
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Публикация
|
|
289
|
+
|
|
290
|
+
Публикация выполняется автоматически через GitHub Actions при создании релиза.
|
|
291
|
+
|
|
292
|
+
| Тип релиза | NPM-тег |
|
|
293
|
+
|-------------|----------|
|
|
294
|
+
| Pre-release | `test` |
|
|
295
|
+
| Release | `latest` |
|
|
296
|
+
|
|
297
|
+
Версия берётся из git-тега и подставляется в `package.json` во время CI-сборки.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## Как дополнять README
|
|
302
|
+
|
|
303
|
+
Добавляйте информацию в соответствующий раздел. Если раздела нет — создайте новый перед этим разделом.
|
|
304
|
+
|
|
305
|
+
**Рекомендации:**
|
|
306
|
+
- Каждый новый компонент — строка в таблицу раздела «Компоненты» с кратким описанием.
|
|
307
|
+
- Каждый новый провайдер — подраздел в «Провайдеры» с примером использования.
|
|
308
|
+
- Новые утилиты — добавить в список импортов раздела «Утилиты».
|
|
309
|
+
- Примеры кода должны быть минимальными и рабочими.
|
|
@@ -40,7 +40,7 @@ const RichFilterTextField = ({ filterExpr, filterExprHist: searchHist, fields, o
|
|
|
40
40
|
if (cursorPos &&
|
|
41
41
|
getHints(value, cursorPos, fields, operators) !== currentHintOptions)
|
|
42
42
|
setFocusIdx(0);
|
|
43
|
-
onChange(value
|
|
43
|
+
onChange(value);
|
|
44
44
|
};
|
|
45
45
|
const insertHint = (hint) => {
|
|
46
46
|
if (cursorPos == null)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubtaskItem.d.ts","sourceRoot":"","sources":["../../../src/components/SubtaskItem/SubtaskItem.tsx"],"names":[],"mappings":"AAgBA,OAAO,gBAAgB,MAAM,oBAAoB,CAAC;AAmBlD,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,YAAY,EACZ,IAAI,EACJ,QAAQ,EACR,UAAU,EACV,uBAAuB,GACxB,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"SubtaskItem.d.ts","sourceRoot":"","sources":["../../../src/components/SubtaskItem/SubtaskItem.tsx"],"names":[],"mappings":"AAgBA,OAAO,gBAAgB,MAAM,oBAAoB,CAAC;AAmBlD,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,YAAY,EACZ,IAAI,EACJ,QAAQ,EACR,UAAU,EACV,uBAAuB,GACxB,EAAE,gBAAgB,2CAsIlB"}
|
|
@@ -12,7 +12,7 @@ import AccordionDetails from "@mui/material/AccordionDetails";
|
|
|
12
12
|
import AccordionSummary from "@mui/material/AccordionSummary";
|
|
13
13
|
import CircularProgress from "@mui/material/CircularProgress";
|
|
14
14
|
import moment from "moment";
|
|
15
|
-
import { useState } from "react";
|
|
15
|
+
import { useEffect, useState } from "react";
|
|
16
16
|
import { taskStateText } from "../../utils";
|
|
17
17
|
const SubtaskState = ({ state }) => {
|
|
18
18
|
switch (state) {
|
|
@@ -34,6 +34,16 @@ export default function SubtaskItem({ taskInstance, logs, expanded, onShowLogs,
|
|
|
34
34
|
const [copyVisible, setCopyVisible] = useState(false);
|
|
35
35
|
const [copyText, setCopyText] = useState("Копировать");
|
|
36
36
|
const theme = useTheme();
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (expanded) {
|
|
39
|
+
setCopyVisible(true);
|
|
40
|
+
setCopyText("Копировать");
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
setCopyVisible(false);
|
|
44
|
+
setCopyText("Копировать");
|
|
45
|
+
}
|
|
46
|
+
}, [expanded]);
|
|
37
47
|
const formatEndDate = new Date(taskInstance.executionDate).toLocaleString("ru", {
|
|
38
48
|
weekday: "short",
|
|
39
49
|
month: "short",
|
|
@@ -51,17 +61,6 @@ export default function SubtaskItem({ taskInstance, logs, expanded, onShowLogs,
|
|
|
51
61
|
});
|
|
52
62
|
return (_jsxs(Accordion, { disableGutters: true, expanded: expanded, onChange: () => onShowLogs(taskInstance.taskId, taskInstance.tryNumber), disabled: !["running", "success", "failed"].includes(taskInstance.state || ""), sx: {
|
|
53
63
|
position: "relative",
|
|
54
|
-
}, slotProps: {
|
|
55
|
-
transition: {
|
|
56
|
-
onEntered: () => {
|
|
57
|
-
setCopyVisible(true);
|
|
58
|
-
setCopyText("Копировать");
|
|
59
|
-
},
|
|
60
|
-
onExit: () => {
|
|
61
|
-
setCopyVisible(false);
|
|
62
|
-
setCopyText("Копировать");
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
64
|
}, children: [_jsx(AccordionSummary, { expandIcon: _jsx(ExpandMore, {}), children: _jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem" }, children: [_jsx(SubtaskState, { state: taskInstance.state }), _jsxs("span", { children: [taskInstance.taskId, " - ", taskStateText(taskInstance.state), taskInstance.state === "success"
|
|
66
65
|
? (formatEndDate.startsWith("вт") ? " во " : " в ") +
|
|
67
66
|
formatEndDate +
|
|
@@ -180,7 +180,7 @@ const TaskLoader = ({ style, subTitle, onUnauthorized }) => {
|
|
|
180
180
|
successHandler: (resp) => {
|
|
181
181
|
if (!resp)
|
|
182
182
|
return;
|
|
183
|
-
onUpdateState(true, resp.dagRun
|
|
183
|
+
onUpdateState(true, resp.dagRun);
|
|
184
184
|
setActiveTask(null);
|
|
185
185
|
setLogs([null, null, null]);
|
|
186
186
|
},
|
|
@@ -211,7 +211,7 @@ const TaskLoader = ({ style, subTitle, onUnauthorized }) => {
|
|
|
211
211
|
successHandler: (resp) => {
|
|
212
212
|
if (!resp)
|
|
213
213
|
return;
|
|
214
|
-
onUpdateState(true, resp.dagRun
|
|
214
|
+
onUpdateState(true, resp.dagRun);
|
|
215
215
|
setActiveTask(null);
|
|
216
216
|
setLogs([null, null, null]);
|
|
217
217
|
setBlockControls(false);
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
type AirflowContextValue = {
|
|
3
|
+
dagId?: string | null;
|
|
4
|
+
airflowAvailable: boolean;
|
|
5
|
+
onChangeDagId(dagId: string | null): void;
|
|
6
|
+
onLostConnection(): void;
|
|
7
|
+
};
|
|
2
8
|
interface AirflowProviderProps {
|
|
3
9
|
children?: React.ReactNode;
|
|
4
10
|
dagId?: string | null;
|
|
5
11
|
onUnauthorized(): void;
|
|
6
12
|
}
|
|
7
13
|
export declare const AirflowProvider: ({ children, dagId: _dagId, onUnauthorized, }: AirflowProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
|
-
export declare const useAirflowContext: () =>
|
|
9
|
-
dagId?: string | null;
|
|
10
|
-
airflowAvailable: boolean;
|
|
11
|
-
onChangeDagId(dagId: string | null): void;
|
|
12
|
-
onLostConnection(): void;
|
|
13
|
-
};
|
|
14
|
+
export declare const useAirflowContext: () => AirflowContextValue;
|
|
14
15
|
export default AirflowProvider;
|
|
15
16
|
//# sourceMappingURL=AirflowProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AirflowProvider.d.ts","sourceRoot":"","sources":["../../src/providers/AirflowProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"AirflowProvider.d.ts","sourceRoot":"","sources":["../../src/providers/AirflowProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAC;AAIf,KAAK,mBAAmB,GAAG;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1C,gBAAgB,IAAI,IAAI,CAAC;CAC1B,CAAC;AAIF,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,cAAc,IAAI,IAAI,CAAC;CACxB;AA+BD,eAAO,MAAM,eAAe,GAAI,8CAI7B,oBAAoB,4CAyDtB,CAAC;AAEF,eAAO,MAAM,iBAAiB,2BAM7B,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -1,66 +1,69 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useCallback, useContext, useState } from "react";
|
|
2
|
+
import { createContext, useCallback, useContext, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import useSWR from "swr";
|
|
4
4
|
import { useSnackbarContext } from "./SnackBarProvider";
|
|
5
|
-
const AirflowContext = createContext(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
const AirflowContext = createContext(null);
|
|
6
|
+
const makeFetcher = (onUnauthorized) => async (url) => {
|
|
7
|
+
const res = await fetch(url);
|
|
8
|
+
let err;
|
|
9
|
+
if (res.status === 401) {
|
|
10
|
+
onUnauthorized();
|
|
11
|
+
err = new Error("Unauthorized Airflow connection checking request.");
|
|
12
|
+
err.status = 401;
|
|
13
|
+
}
|
|
14
|
+
else if (!res.ok && res.status !== 404) {
|
|
15
|
+
err = new Error("An error occurred while fetching the data.");
|
|
16
|
+
err.info = await res.json();
|
|
17
|
+
err.status = res.status;
|
|
18
|
+
}
|
|
19
|
+
if (typeof err !== "undefined") {
|
|
20
|
+
throw err;
|
|
21
|
+
}
|
|
22
|
+
return res.json();
|
|
23
|
+
};
|
|
11
24
|
export const AirflowProvider = ({ children, dagId: _dagId, onUnauthorized, }) => {
|
|
12
|
-
const fetcher =
|
|
13
|
-
const res = await fetch(url);
|
|
14
|
-
if (res.status === 401) {
|
|
15
|
-
onUnauthorized();
|
|
16
|
-
}
|
|
17
|
-
if (!res.ok && res.status !== 404) {
|
|
18
|
-
const error = new Error("An error occurred while fetching the data.");
|
|
19
|
-
throw { ...error, info: await res.json(), status: res.status };
|
|
20
|
-
}
|
|
21
|
-
return res.json();
|
|
22
|
-
};
|
|
25
|
+
const fetcher = useMemo(() => makeFetcher(onUnauthorized), [onUnauthorized]);
|
|
23
26
|
const { enqueueSnackbar } = useSnackbarContext();
|
|
24
27
|
const [dagId, setDagId] = useState(_dagId);
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const { error, mutate } = useSWR("airflow/check_connection", fetcher, {
|
|
28
|
+
// null = первый запрос ещё не завершён
|
|
29
|
+
const prevAvailable = useRef(null);
|
|
30
|
+
const { error, isLoading, mutate } = useSWR("airflow/check_connection", fetcher, {
|
|
28
31
|
refreshInterval: 10000,
|
|
29
32
|
onSuccess: () => {
|
|
30
|
-
if (
|
|
31
|
-
setFirstRun(false);
|
|
32
|
-
}
|
|
33
|
-
else if (!previousAirflowAvailable) {
|
|
33
|
+
if (prevAvailable.current === false) {
|
|
34
34
|
enqueueSnackbar("Соединение с Airflow установлено!", "info");
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
prevAvailable.current = true;
|
|
37
37
|
},
|
|
38
38
|
onError: () => {
|
|
39
|
-
if (
|
|
40
|
-
enqueueSnackbar(
|
|
41
|
-
setFirstRun(false);
|
|
39
|
+
if (prevAvailable.current === null) {
|
|
40
|
+
enqueueSnackbar("Не удалось установить соединение с Airflow!", "warning");
|
|
42
41
|
}
|
|
43
|
-
else if (
|
|
44
|
-
enqueueSnackbar(
|
|
42
|
+
else if (prevAvailable.current === true) {
|
|
43
|
+
enqueueSnackbar("Потеряно соединение с Airflow!", "warning");
|
|
45
44
|
}
|
|
46
|
-
|
|
45
|
+
prevAvailable.current = false;
|
|
47
46
|
},
|
|
48
47
|
});
|
|
49
|
-
const airflowAvailable = error == null;
|
|
50
|
-
const
|
|
48
|
+
const airflowAvailable = !isLoading && error == null;
|
|
49
|
+
const onChangeDagId = useCallback((dagId) => {
|
|
51
50
|
setDagId(dagId);
|
|
52
|
-
}, [
|
|
53
|
-
const onLostConnection = () => {
|
|
54
|
-
mutate();
|
|
55
|
-
};
|
|
51
|
+
}, []);
|
|
52
|
+
const onLostConnection = useCallback(() => {
|
|
53
|
+
void mutate();
|
|
54
|
+
}, [mutate]);
|
|
56
55
|
return (_jsx(AirflowContext.Provider, { value: {
|
|
57
56
|
airflowAvailable,
|
|
58
57
|
dagId,
|
|
59
|
-
onChangeDagId
|
|
58
|
+
onChangeDagId,
|
|
60
59
|
onLostConnection,
|
|
61
60
|
}, children: children }));
|
|
62
61
|
};
|
|
63
62
|
export const useAirflowContext = () => {
|
|
64
|
-
|
|
63
|
+
const ctx = useContext(AirflowContext);
|
|
64
|
+
if (ctx === null) {
|
|
65
|
+
throw new Error("useAirflowContext must be used within AirflowProvider");
|
|
66
|
+
}
|
|
67
|
+
return ctx;
|
|
65
68
|
};
|
|
66
69
|
export default AirflowProvider;
|
|
@@ -4,19 +4,19 @@ interface DagRunStateProviderProps {
|
|
|
4
4
|
children?: React.ReactNode;
|
|
5
5
|
dagRun?: DagRun;
|
|
6
6
|
autorun?: boolean;
|
|
7
|
-
|
|
7
|
+
pollingByUser?: boolean;
|
|
8
8
|
taskInstances?: TaskInstances;
|
|
9
9
|
autorunHandler?(dagRun: DagRun | null, autorun: boolean): void;
|
|
10
10
|
onUnauthorized(): void;
|
|
11
11
|
}
|
|
12
|
-
declare const DagRunStateProvider: ({ children, dagRun: _dagRun, autorun: _autorun,
|
|
12
|
+
declare const DagRunStateProvider: ({ children, dagRun: _dagRun, autorun: _autorun, pollingByUser, autorunHandler, onUnauthorized, }: DagRunStateProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
13
|
export declare const useDagRunStateContext: () => {
|
|
14
14
|
isActive: boolean;
|
|
15
15
|
autorun: boolean;
|
|
16
16
|
dagRun: DagRun | null;
|
|
17
17
|
taskInstances: TaskInstances | null;
|
|
18
18
|
dagStartedOnce: boolean;
|
|
19
|
-
onUpdateState(isActive: boolean, dagRun: DagRun | null
|
|
19
|
+
onUpdateState(isActive: boolean, dagRun: DagRun | null): void;
|
|
20
20
|
error: boolean;
|
|
21
21
|
onDagStarted: () => void;
|
|
22
22
|
activate(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DagStateProvider.d.ts","sourceRoot":"","sources":["../../src/providers/DagStateProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"DagStateProvider.d.ts","sourceRoot":"","sources":["../../src/providers/DagStateProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAkBjD,UAAU,wBAAwB;IAChC,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,cAAc,CAAC,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAC/D,cAAc,IAAI,IAAI,CAAC;CACxB;AAED,QAAA,MAAM,mBAAmB,GAAI,kGAO1B,wBAAwB,4CAwJ1B,CAAC;AAEF,eAAO,MAAM,qBAAqB;cAxLtB,OAAO;aACR,OAAO;YACR,MAAM,GAAG,IAAI;mBACN,aAAa,GAAG,IAAI;oBACnB,OAAO;4BACC,OAAO,UAAU,MAAM,GAAG,IAAI,GAAG,IAAI;WACtD,OAAO;kBACA,MAAM,IAAI;gBACZ,IAAI;kBACF,IAAI;wBACE,OAAO,GAAG,IAAI;CAsLnC,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
|
|
@@ -3,41 +3,38 @@ import { createContext, useCallback, useContext, useEffect, useEffectEvent, useS
|
|
|
3
3
|
import useSWR from "swr";
|
|
4
4
|
import { useAirflowContext } from "./AirflowProvider";
|
|
5
5
|
import { useSnackbarContext } from "./SnackBarProvider";
|
|
6
|
-
const DagRunStateContext = createContext(
|
|
7
|
-
|
|
8
|
-
autorun: false,
|
|
9
|
-
dagRun: null,
|
|
10
|
-
taskInstances: null,
|
|
11
|
-
dagStartedOnce: false,
|
|
12
|
-
error: false,
|
|
13
|
-
onUpdateState: () => { },
|
|
14
|
-
onDagStarted: () => { },
|
|
15
|
-
fetchState: () => { },
|
|
16
|
-
activate: () => { },
|
|
17
|
-
setAutorun: () => { },
|
|
18
|
-
});
|
|
19
|
-
const DagRunStateProvider = ({ children, dagRun: _dagRun, autorun: _autorun, poolingByUser, autorunHandler, onUnauthorized, }) => {
|
|
6
|
+
const DagRunStateContext = createContext(null);
|
|
7
|
+
const DagRunStateProvider = ({ children, dagRun: _dagRun, autorun: _autorun, pollingByUser, autorunHandler, onUnauthorized, }) => {
|
|
20
8
|
const { airflowAvailable, dagId, onLostConnection } = useAirflowContext();
|
|
21
|
-
const [autorun, setAutorun] = useState(_autorun
|
|
22
|
-
const [dagRunId, setDagRunId] = useState(_dagRun?.dagRunId);
|
|
9
|
+
const [autorun, setAutorun] = useState(_autorun ?? false);
|
|
10
|
+
const [dagRunId, setDagRunId] = useState(_dagRun?.dagRunId ?? null);
|
|
23
11
|
const [dagStartedOnce, setDagStartedOnce] = useState(false);
|
|
24
12
|
const { enqueueSnackbar } = useSnackbarContext();
|
|
25
|
-
const defaultFetcher = async ([url, dagId, dagRunId,
|
|
13
|
+
const defaultFetcher = useCallback(async ([url, dagId, dagRunId, pollingByUser]) => {
|
|
26
14
|
const requestUrl = new URL("http://foo/" + url);
|
|
27
15
|
requestUrl.searchParams.append("dag_id", dagId);
|
|
28
16
|
if (dagRunId)
|
|
29
17
|
requestUrl.searchParams.append("dag_run_id", dagRunId);
|
|
30
|
-
requestUrl.searchParams.append("by_user", String(
|
|
18
|
+
requestUrl.searchParams.append("by_user", String(pollingByUser));
|
|
31
19
|
const resultUrl = requestUrl.pathname.substring(1) + requestUrl.search;
|
|
32
|
-
const res = await fetch(resultUrl, {
|
|
20
|
+
const res = await fetch(resultUrl, {
|
|
21
|
+
signal: AbortSignal.timeout(10000),
|
|
22
|
+
});
|
|
33
23
|
if (res.status === 401) {
|
|
34
24
|
onUnauthorized();
|
|
35
25
|
}
|
|
36
26
|
if (!res.ok) {
|
|
37
|
-
|
|
27
|
+
let body = {};
|
|
28
|
+
try {
|
|
29
|
+
body = await res.json();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* ignore non-JSON bodies (e.g. nginx error pages) */
|
|
33
|
+
}
|
|
34
|
+
throw { ...body, status: res.status };
|
|
38
35
|
}
|
|
39
36
|
return res.json();
|
|
40
|
-
};
|
|
37
|
+
}, [onUnauthorized]);
|
|
41
38
|
const [shouldRevalidate, setShouldRevalidate] = useState(true);
|
|
42
39
|
const activate = useCallback(() => {
|
|
43
40
|
setShouldRevalidate(true);
|
|
@@ -47,12 +44,12 @@ const DagRunStateProvider = ({ children, dagRun: _dagRun, autorun: _autorun, poo
|
|
|
47
44
|
"airflow/dag_run",
|
|
48
45
|
dagId,
|
|
49
46
|
dagRunId ?? null,
|
|
50
|
-
Boolean(
|
|
47
|
+
Boolean(pollingByUser),
|
|
51
48
|
], defaultFetcher, {
|
|
52
49
|
refreshInterval: shouldRevalidate ? 1000 : 0,
|
|
53
50
|
onError: (err) => {
|
|
54
|
-
if (err?.status !== 404) {
|
|
55
|
-
enqueueSnackbar(`Ошибка при обновлении состояния задачи${
|
|
51
|
+
if (err?.status !== 404 && err?.status !== 401) {
|
|
52
|
+
enqueueSnackbar(`Ошибка при обновлении состояния задачи${dagId ? ` ${dagId}` : ""}!`, "error");
|
|
56
53
|
}
|
|
57
54
|
if (err?.status === 503) {
|
|
58
55
|
onLostConnection();
|
|
@@ -65,15 +62,14 @@ const DagRunStateProvider = ({ children, dagRun: _dagRun, autorun: _autorun, poo
|
|
|
65
62
|
});
|
|
66
63
|
const dagRun = data?.dagRun ?? null;
|
|
67
64
|
const taskInstances = data?.taskInstances ?? null;
|
|
68
|
-
const
|
|
65
|
+
const updDagRunId = useEffectEvent((newDagRunId) => {
|
|
69
66
|
if (dagRunId !== newDagRunId) {
|
|
70
67
|
setDagRunId(newDagRunId);
|
|
71
68
|
}
|
|
72
69
|
});
|
|
73
70
|
// Обновлять идентификатор запуска при изменении
|
|
74
71
|
useEffect(() => {
|
|
75
|
-
|
|
76
|
-
console.log("effect 1");
|
|
72
|
+
updDagRunId(dagRun?.dagRunId ?? null);
|
|
77
73
|
}, [dagRun?.dagRunId ?? null]);
|
|
78
74
|
const autorunAction = useEffectEvent((dagRun) => {
|
|
79
75
|
if (autorunHandler) {
|
|
@@ -89,23 +85,19 @@ const DagRunStateProvider = ({ children, dagRun: _dagRun, autorun: _autorun, poo
|
|
|
89
85
|
if (dagRun?.state === "success") {
|
|
90
86
|
autorunAction(dagRun);
|
|
91
87
|
}
|
|
92
|
-
console.log("effect 2");
|
|
93
88
|
}, [dagRun?.state]);
|
|
94
|
-
const onUpdateState = (isActive, newDagRun
|
|
89
|
+
const onUpdateState = (isActive, newDagRun) => {
|
|
95
90
|
setShouldRevalidate(isActive);
|
|
96
|
-
setDagRunId(newDagRun?.dagRunId);
|
|
91
|
+
setDagRunId(newDagRun?.dagRunId ?? null);
|
|
97
92
|
if (newDagRun == null)
|
|
98
93
|
mutate();
|
|
99
94
|
};
|
|
100
|
-
|
|
95
|
+
/** Вызывать сразу после запуска дага */
|
|
101
96
|
const onDagStarted = () => {
|
|
102
|
-
// setIsActive(true);
|
|
103
97
|
setDagRunId(null);
|
|
104
|
-
// setTaskInstances(null);
|
|
105
98
|
setShouldRevalidate(true);
|
|
106
99
|
setDagStartedOnce(true);
|
|
107
100
|
};
|
|
108
|
-
const updateState = () => mutate();
|
|
109
101
|
return (_jsx(DagRunStateContext.Provider, { value: {
|
|
110
102
|
isActive: shouldRevalidate,
|
|
111
103
|
autorun,
|
|
@@ -116,11 +108,15 @@ const DagRunStateProvider = ({ children, dagRun: _dagRun, autorun: _autorun, poo
|
|
|
116
108
|
onDagStarted,
|
|
117
109
|
activate,
|
|
118
110
|
error: Boolean(error),
|
|
119
|
-
fetchState:
|
|
120
|
-
setAutorun
|
|
111
|
+
fetchState: () => mutate(),
|
|
112
|
+
setAutorun,
|
|
121
113
|
}, children: children }));
|
|
122
114
|
};
|
|
123
115
|
export const useDagRunStateContext = () => {
|
|
124
|
-
|
|
116
|
+
const ctx = useContext(DagRunStateContext);
|
|
117
|
+
if (!ctx) {
|
|
118
|
+
throw new Error("useDagRunStateContext must be used within DagRunStateProvider");
|
|
119
|
+
}
|
|
120
|
+
return ctx;
|
|
125
121
|
};
|
|
126
122
|
export default DagRunStateProvider;
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import React from "react";
|
|
2
1
|
interface SessionProviderProps {
|
|
3
2
|
children?: React.ReactNode;
|
|
4
3
|
pollingInterval: number;
|
|
5
|
-
autoRedirect?: boolean;
|
|
6
4
|
onUnauthorized?(): void;
|
|
7
5
|
}
|
|
8
|
-
declare const SessionProvider: ({ children, pollingInterval,
|
|
6
|
+
declare const SessionProvider: ({ children, pollingInterval, onUnauthorized, }: SessionProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
7
|
export declare const useSessionContext: () => {
|
|
10
8
|
expiresIn: number;
|
|
11
9
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionProvider.d.ts","sourceRoot":"","sources":["../../src/providers/SessionProvider.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"SessionProvider.d.ts","sourceRoot":"","sources":["../../src/providers/SessionProvider.tsx"],"names":[],"mappings":"AAIA,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,IAAI,IAAI,CAAC;CACzB;AAQD,QAAA,MAAM,eAAe,GAAI,gDAItB,oBAAoB,4CA8CtB,CAAC;AAEF,eAAO,MAAM,iBAAiB;eAzDjB,MAAM;CA2DlB,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -5,24 +5,24 @@ import { useSnackbarContext } from "./SnackBarProvider";
|
|
|
5
5
|
const SessionContext = createContext({
|
|
6
6
|
expiresIn: 0,
|
|
7
7
|
});
|
|
8
|
-
const SessionProvider = ({ children, pollingInterval,
|
|
8
|
+
const SessionProvider = ({ children, pollingInterval, onUnauthorized, }) => {
|
|
9
9
|
const { enqueueSnackbar } = useSnackbarContext();
|
|
10
10
|
const [expires, setExpires] = useState(false);
|
|
11
|
+
const [expiresIn, setExpiresIn] = useState(0);
|
|
11
12
|
const alertExpire = () => enqueueSnackbar("Сессия завершилась, перезагрузите страницу!", "default");
|
|
12
13
|
const fetchSessionExpiryAge = () => getRequest({
|
|
13
14
|
url: "base/check_session",
|
|
14
|
-
successHandler: (
|
|
15
|
+
successHandler: (resp) => {
|
|
16
|
+
setExpiresIn(resp.sessionExpiresIn);
|
|
17
|
+
},
|
|
15
18
|
errorHandler: (e) => {
|
|
16
|
-
if (e?.status === 401
|
|
17
|
-
onUnauthorized();
|
|
19
|
+
if (e?.status === 401) {
|
|
20
|
+
onUnauthorized?.();
|
|
18
21
|
}
|
|
19
22
|
setExpires(true);
|
|
20
23
|
alertExpire();
|
|
21
24
|
},
|
|
22
|
-
}).catch((
|
|
23
|
-
if (e?.status === 401 && autoRedirect && onUnauthorized) {
|
|
24
|
-
onUnauthorized();
|
|
25
|
-
}
|
|
25
|
+
}).catch(() => {
|
|
26
26
|
setExpires(true);
|
|
27
27
|
alertExpire();
|
|
28
28
|
});
|
|
@@ -35,7 +35,7 @@ const SessionProvider = ({ children, pollingInterval, autoRedirect, onUnauthoriz
|
|
|
35
35
|
return () => clearInterval(interval);
|
|
36
36
|
}, [expires]);
|
|
37
37
|
return (_jsx(SessionContext.Provider, { value: {
|
|
38
|
-
expiresIn
|
|
38
|
+
expiresIn,
|
|
39
39
|
}, children: children }));
|
|
40
40
|
};
|
|
41
41
|
export const useSessionContext = () => {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
type SnackbarVariant = "default" | "error" | "success" | "warning" | "info";
|
|
2
3
|
interface SnackBarProps {
|
|
3
|
-
children?: React.ReactNode
|
|
4
|
+
children?: React.ReactNode;
|
|
4
5
|
}
|
|
5
6
|
declare const SnackBarProvider: ({ children }: SnackBarProps) => import("react/jsx-runtime").JSX.Element;
|
|
6
7
|
export declare const useSnackbarContext: () => {
|
|
7
|
-
enqueueSnackbar(message: string, variant:
|
|
8
|
+
enqueueSnackbar(message: string, variant: SnackbarVariant): void;
|
|
8
9
|
};
|
|
9
10
|
export default SnackBarProvider;
|
|
10
11
|
//# sourceMappingURL=SnackBarProvider.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SnackBarProvider.d.ts","sourceRoot":"","sources":["../../src/providers/SnackBarProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"SnackBarProvider.d.ts","sourceRoot":"","sources":["../../src/providers/SnackBarProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA0D,MAAM,OAAO,CAAC;AAM/E,KAAK,eAAe,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;AAY5E,UAAU,aAAa;IACrB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAoDD,QAAA,MAAM,gBAAgB,GAAI,cAAc,aAAa,4CAMpD,CAAC;AAEF,eAAO,MAAM,kBAAkB;6BAvEJ,MAAM,WAAW,eAAe,GAAG,IAAI;CAyEjE,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
|
|
@@ -1,29 +1,36 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import { createContext, useContext } from "react";
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useMemo } from "react";
|
|
3
3
|
import Button from "@mui/material/Button";
|
|
4
4
|
import { SnackbarProvider, useSnackbar } from "notistack";
|
|
5
5
|
const SnackBarContext = createContext({
|
|
6
|
+
// Намеренный no-op по умолчанию: позволяет вызывать useSnackbarContext()
|
|
7
|
+
// вне провайдера без исключения. Это нужно для обратной совместимости:
|
|
8
|
+
// другие провайдеры (AirflowProvider, DagStateProvider) мокируют
|
|
9
|
+
// SnackBarProvider в своих тестах и ожидают, что хук не бросает ошибку.
|
|
6
10
|
enqueueSnackbar: () => { },
|
|
7
11
|
});
|
|
8
12
|
const SnackBarProviderUtilities = ({ children }) => {
|
|
9
13
|
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
// closeSnackbar стабилен (гарантия notistack), поэтому action создаётся один раз.
|
|
15
|
+
const action = useCallback((snackbarId) => (_jsx(Button, { size: "small", variant: "text", color: "inherit", onClick: () => {
|
|
16
|
+
closeSnackbar(snackbarId);
|
|
17
|
+
}, children: "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" })), [closeSnackbar]);
|
|
18
|
+
// enqueueSnackbar и action оба стабильны, поэтому callback создаётся один раз.
|
|
19
|
+
// preventDuplicate не передаём — он уже задан на уровне <SnackbarProvider>.
|
|
20
|
+
const handleEnqueueSnackbar = useCallback((message, variant) => {
|
|
14
21
|
enqueueSnackbar(message, {
|
|
15
|
-
variant
|
|
16
|
-
|
|
17
|
-
action: action,
|
|
22
|
+
variant,
|
|
23
|
+
action,
|
|
18
24
|
anchorOrigin: {
|
|
19
25
|
horizontal: "left",
|
|
20
26
|
vertical: "bottom",
|
|
21
27
|
},
|
|
22
28
|
});
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
}, [enqueueSnackbar, action]);
|
|
30
|
+
// useMemo предотвращает создание нового объекта value на каждом рендере,
|
|
31
|
+
// что вызвало бы лишние ре-рендеры у всех потребителей контекста.
|
|
32
|
+
const contextValue = useMemo(() => ({ enqueueSnackbar: handleEnqueueSnackbar }), [handleEnqueueSnackbar]);
|
|
33
|
+
return (_jsx(SnackBarContext.Provider, { value: contextValue, children: children }));
|
|
27
34
|
};
|
|
28
35
|
const SnackBarProvider = ({ children }) => {
|
|
29
36
|
return (_jsx(SnackbarProvider, { maxSnack: 5, autoHideDuration: 5000, preventDuplicate: true, children: _jsx(SnackBarProviderUtilities, { children: children }) }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danikokonn/yarik-frontend-lib",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "",
|
|
6
6
|
"author": "",
|
|
@@ -14,52 +14,60 @@
|
|
|
14
14
|
},
|
|
15
15
|
"devDependencies": {
|
|
16
16
|
"@eslint/js": "^9.34.0",
|
|
17
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
18
|
+
"@testing-library/react": "^16.3.2",
|
|
19
|
+
"@testing-library/user-event": "^14.6.1",
|
|
17
20
|
"@types/lodash.debounce": "^4.0.9",
|
|
18
21
|
"@types/moment-duration-format": "^2.2.7",
|
|
19
|
-
"@types/react": "^19.2.
|
|
22
|
+
"@types/react": "^19.2.14",
|
|
20
23
|
"@types/react-dom": "^19.2.3",
|
|
21
|
-
"
|
|
24
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
25
|
+
"eslint": "^9.39.4",
|
|
22
26
|
"eslint-plugin-react": "^7.37.5",
|
|
23
27
|
"eslint-plugin-unicorn": "^62.0.0",
|
|
24
28
|
"globals": "^16.5.0",
|
|
29
|
+
"jsdom": "^29.0.1",
|
|
25
30
|
"prettier": "3.6.2",
|
|
26
|
-
"react": "^19.2.
|
|
27
|
-
"react-dom": "^19.2.
|
|
31
|
+
"react": "^19.2.4",
|
|
32
|
+
"react-dom": "^19.2.4",
|
|
28
33
|
"ts-loader": "^9.5.4",
|
|
29
34
|
"tss-react": "^4.9.20",
|
|
30
35
|
"typescript": "^5.9.3",
|
|
31
|
-
"typescript-eslint": "^8.
|
|
36
|
+
"typescript-eslint": "^8.57.1",
|
|
37
|
+
"vitest": "^4.1.0"
|
|
32
38
|
},
|
|
33
39
|
"dependencies": {
|
|
34
40
|
"@emotion/react": "^11.14.0",
|
|
35
41
|
"@emotion/styled": "^11.14.1",
|
|
36
|
-
"@mui/icons-material": "^7.3.
|
|
37
|
-
"@mui/material": "^7.3.
|
|
38
|
-
"@mui/x-date-pickers": "^8.
|
|
42
|
+
"@mui/icons-material": "^7.3.9",
|
|
43
|
+
"@mui/material": "^7.3.9",
|
|
44
|
+
"@mui/x-date-pickers": "^8.27.2",
|
|
39
45
|
"@tsconfig/strictest": "^2.0.8",
|
|
40
|
-
"css-loader": "^7.1.
|
|
46
|
+
"css-loader": "^7.1.4",
|
|
41
47
|
"moment": "^2.30.1",
|
|
42
48
|
"moment-duration-format": "^2.3.2",
|
|
43
|
-
"moment-timezone": "^0.6.
|
|
49
|
+
"moment-timezone": "^0.6.1",
|
|
44
50
|
"notistack": "^3.0.2",
|
|
45
51
|
"react-imask": "^7.6.1",
|
|
46
52
|
"react-json-tree": "^0.20.0",
|
|
47
|
-
"react-router": "^7.
|
|
48
|
-
"react-virtuoso": "^4.
|
|
53
|
+
"react-router": "^7.13.1",
|
|
54
|
+
"react-virtuoso": "^4.18.3",
|
|
49
55
|
"style-loader": "^4.0.0",
|
|
50
|
-
"swr": "^2.
|
|
51
|
-
"transliteration": "^2.
|
|
56
|
+
"swr": "^2.4.1",
|
|
57
|
+
"transliteration": "^2.6.1"
|
|
52
58
|
},
|
|
53
59
|
"peerDependencies": {
|
|
54
60
|
"react": ">=18 <20",
|
|
55
61
|
"react-dom": ">=18 <20"
|
|
56
62
|
},
|
|
57
63
|
"scripts": {
|
|
58
|
-
"test": "
|
|
64
|
+
"test": "vitest",
|
|
65
|
+
"test:run": "vitest run",
|
|
66
|
+
"coverage": "vitest run --coverage",
|
|
59
67
|
"build": "tsc"
|
|
60
68
|
},
|
|
61
|
-
"main": "
|
|
62
|
-
"types": "
|
|
69
|
+
"main": "./dist/index.js",
|
|
70
|
+
"types": "./dist/index.d.ts",
|
|
63
71
|
"exports": {
|
|
64
72
|
".": {
|
|
65
73
|
"import": "./dist/index.js",
|