refactor: Линзы, API, Docker

- Обновлена логика фильтрации и сортировки линз
- Добавлена возможность поиска
- Добавлены переменные окружения
- Удален secrets-example.yaml
This commit is contained in:
Андрей Дувакин 2025-06-08 09:38:51 +05:00
parent 05bfec81fc
commit 914ae0528f
10 changed files with 152 additions and 109 deletions

View File

@ -1,3 +1,4 @@
.venv
k8s
.idea
.idea
.env

View File

@ -1,6 +1,6 @@
from typing import Sequence, Optional, Tuple, Literal
from sqlalchemy import select, desc, or_, func
from sqlalchemy import select, desc, or_, func, String
from sqlalchemy.ext.asyncio import AsyncSession
from app.domain.models import Lens
@ -11,18 +11,18 @@ class LensesRepository:
self.db = db
async def get_all(
self,
skip: int = 0,
limit: int = 10,
search: str = None,
sort_order: Literal["asc", "desc"] = "desc",
tor: float = None,
diameter: float = None,
preset_refraction: float = None,
periphery_toricity: float = None,
side: str = "all",
issued: bool = None,
trial: float = None,
self,
skip: int = 0,
limit: int = 10,
search: str = None,
sort_order: Literal["asc", "desc"] = "desc",
tor: float = None,
diameter: float = None,
preset_refraction: float = None,
periphery_toricity: float = None,
side: str = "all",
issued: bool = None,
trial: float = None,
) -> Tuple[Sequence[Lens], int]:
stmt = select(Lens)
count_stmt = select(func.count()).select_from(Lens)
@ -31,24 +31,26 @@ class LensesRepository:
search = f"%{search}%"
stmt = stmt.filter(
or_(
Lens.tor.cast(str).ilike(search),
Lens.diameter.cast(str).ilike(search),
Lens.preset_refraction.cast(str).ilike(search),
Lens.periphery_toricity.cast(str).ilike(search),
Lens.side.ilike(search),
Lens.fvc.cast(str).ilike(search),
Lens.trial.cast(str).ilike(search),
Lens.tor.cast(String).ilike(search),
Lens.diameter.cast(String).ilike(search),
Lens.preset_refraction.cast(String).ilike(search),
Lens.periphery_toricity.cast(String).ilike(search),
Lens.side.cast(String).ilike(search),
Lens.fvc.cast(String).ilike(search),
Lens.trial.cast(String).ilike(search),
Lens.esa.cast(String).ilike(search),
)
)
count_stmt = count_stmt.filter(
or_(
Lens.tor.cast(str).ilike(search),
Lens.diameter.cast(str).ilike(search),
Lens.preset_refraction.cast(str).ilike(search),
Lens.periphery_toricity.cast(str).ilike(search),
Lens.side.ilike(search),
Lens.fvc.cast(str).ilike(search),
Lens.trial.cast(str).ilike(search),
Lens.tor.cast(String).ilike(search),
Lens.diameter.cast(String).ilike(search),
Lens.preset_refraction.cast(String).ilike(search),
Lens.periphery_toricity.cast(String).ilike(search),
Lens.side.cast(String).ilike(search),
Lens.fvc.cast(String).ilike(search),
Lens.trial.cast(String).ilike(search),
Lens.esa.cast(String).ilike(search),
)
)

View File

@ -3,7 +3,6 @@ import uuid
from typing import Optional
import aiofiles
import magic
from fastapi import UploadFile, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.responses import FileResponse

View File

@ -24,6 +24,16 @@ spec:
secretKeyRef:
name: visus-api-secret
key: SECRET_KEY
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: visus-api-secret
key: DATABASE_URL
- name: ALGORITHM
valueFrom:
secretKeyRef:
name: visus-api-secret
key: ALGORITHM
volumeMounts:
- name: uploads-volume
mountPath: {{ .Values.persistence.uploads.containerPath }}

View File

@ -3,4 +3,5 @@ npm-debug.log
build
.dockerignore
.git
k8s
k8s
.env

View File

@ -1,8 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: visus-api-secret
type: Opaque
data:
SECRET_KEY:
DATABASE_URL:

View File

@ -8,9 +8,30 @@ export const lensesApi = createApi({
tagTypes: ['Lens'],
endpoints: (builder) => ({
getLenses: builder.query({
query: () => '/lenses/',
providesTags: ['Lens'],
refetchOnMountOrArgChange: 5
query: ({page, pageSize, search, sortOrder, searchParams}) => ({
url: '/lenses/',
params: {
page,
page_size: pageSize,
search: search || undefined,
sort_order: sortOrder || undefined,
tor: searchParams.tor || undefined,
diameter: searchParams.diameter || undefined,
preset_refraction: searchParams.preset_refraction || undefined,
periphery_toricity: searchParams.periphery_toricity || undefined,
side: searchParams.side !== 'all' ? searchParams.side : undefined,
issued: searchParams.issued !== 'all' ? searchParams.issued : undefined,
trial: searchParams.trial || undefined,
},
}),
providesTags: ['Lenses'],
transformResponse: (response) => {
if (!response || !Array.isArray(response.lenses)) {
console.warn('Unexpected lenses API response:', response);
return {lenses: [], total_count: 0};
}
return response;
},
}),
getNotIssuedLenses: builder.query({
query: () => '/lenses/not_issued/',

View File

@ -148,8 +148,8 @@ const LensesTab = () => {
<Col xs={24} md={24} sm={24} xl={15}>
<Input
placeholder="Поиск линзы"
value={lensesData.searchText}
onChange={(e) => lensesData.handleSetSearchText(e.target.value)}
value={lensesData.tempSearchText}
onChange={(e) => lensesData.handleSetTempSearchText(e.target.value)}
style={lensesData.formItemStyle}
allowClear
/>

View File

@ -1,4 +1,4 @@
import { useEffect, useMemo } from "react";
import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { notification } from "antd";
import {
@ -16,14 +16,14 @@ import {
setSearchParams,
setSearchText,
setShowAdvancedSearch,
setViewMode
setViewMode,
setSortOrder
} from "../../../../../Redux/Slices/lensesSlice.js";
import { getCachedInfo } from "../../../../../Utils/cachedInfoUtils.js";
const useLenses = () => {
const dispatch = useDispatch();
const {
searchText,
viewMode,
currentPage,
pageSize,
@ -31,16 +31,36 @@ const useLenses = () => {
isModalVisible,
showAdvancedSearch,
searchParams,
sortOrder,
} = useSelector(state => state.lensesUI);
const { data = { lenses: [], total_count: 0 }, isLoading, isError } = useGetLensesQuery(
{ page: currentPage, pageSize, search: searchText || undefined },
const [tempSearchText, setTempSearchText] = useState("");
const { data = { lenses: [], total_count: 0 }, isLoading, isError, error } = useGetLensesQuery(
{
page: currentPage,
pageSize,
search: tempSearchText || undefined,
sortOrder,
searchParams,
},
{
pollingInterval: 20000,
refetchOnMountOrArgChange: true,
}
);
useEffect(() => {
if (isError) {
console.error('useGetLensesQuery error:', error);
notification.error({
message: "Ошибка загрузки данных",
description: error?.data?.detail || "Не удалось загрузить линзы",
placement: "topRight",
});
}
}, [isError, error]);
const [addLens, { isLoading: isAdding }] = useAddLensMutation();
const [updateLens, { isLoading: isUpdating }] = useUpdateLensMutation();
const [deleteLens, { isLoading: isDeleting }] = useDeleteLensMutation();
@ -58,9 +78,24 @@ const useLenses = () => {
const tableActionButtonsStyle = { display: "flex", gap: "8px" };
const advancedSearchCardStyle = { marginBottom: 20 };
const handleSetSearchText = (value) => {
dispatch(setSearchText(value));
dispatch(setCurrentPage(1)); // Сбрасываем на первую страницу при изменении поиска
const handleSetTempSearchText = (value) => {
setTempSearchText(value);
};
const handleSearch = () => {
dispatch(setSearchText(tempSearchText));
dispatch(setCurrentPage(1));
};
const handleClearSearch = () => {
setTempSearchText('');
dispatch(setSearchText(''));
dispatch(setCurrentPage(1));
};
const handleSetSortOrder = (value) => {
dispatch(setSortOrder(value));
dispatch(setCurrentPage(1));
};
const handleCloseModal = () => dispatch(closeModal());
@ -85,6 +120,7 @@ const useLenses = () => {
} else {
dispatch(setSearchParams({ ...searchParams, [param]: value }));
}
dispatch(setCurrentPage(1));
};
const toggleAdvancedSearch = () => {
@ -140,34 +176,10 @@ const useLenses = () => {
}
};
const filteredLenses = useMemo(() => {
return data.lenses.filter((lens) => {
const textMatch = Object.values(lens).some((value) =>
value?.toString().toLowerCase().includes(searchText.toLowerCase())
);
const advancedMatch = Object.entries(searchParams).every(([key, value]) => {
if (value === null || value === '') return true;
if (key === 'side') {
if (value === 'all') return true;
return lens.side === value;
}
if (key === 'issued') {
return lens.issued === value || value === "all";
}
return lens[key] === value;
});
return textMatch && advancedMatch && (searchParams.issued || lens.issued === false);
}).sort((a, b) => {
return b.id - a.id; // Сортировка по убыванию id
});
}, [data.lenses, searchText, searchParams]);
const pagination = {
current: currentPage,
pageSize: pageSize,
total: data.total_count,
total: data.total_count || 0,
showSizeChanger: true,
pageSizeOptions: ["5", "10", "20", "50"],
onChange: (page, newPageSize) => {
@ -176,12 +188,12 @@ const useLenses = () => {
};
return {
lenses: data.lenses,
filteredLenses: filteredLenses.map(lens => ({ ...lens, key: lens.id })),
lenses: data.lenses || [],
filteredLenses: (data.lenses || []).map(lens => ({ ...lens, key: lens.id })),
isLoading,
isError,
isProcessing: isAdding || isUpdating || isDeleting,
searchText,
tempSearchText,
viewMode,
currentPage,
pageSize,
@ -189,6 +201,7 @@ const useLenses = () => {
isModalVisible,
showAdvancedSearch,
searchParams,
sortOrder,
pagination,
containerStyle,
filterBarStyle,
@ -196,7 +209,10 @@ const useLenses = () => {
viewModIconStyle,
tableActionButtonsStyle,
advancedSearchCardStyle,
handleSetSearchText,
handleSetTempSearchText,
handleSearch,
handleClearSearch,
handleSetSortOrder,
handleAddLens,
handleEditLens,
handleCloseModal,

View File

@ -1,12 +1,11 @@
import {createSlice} from '@reduxjs/toolkit'
import {cacheInfo} from "../../Utils/cachedInfoUtils.js";
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
searchText: '',
viewMode: 'tile',
currentPage: 1,
pageSize: 10,
pageSize: 5,
sortOrder: 'desc',
selectedLens: null,
isModalVisible: false,
showAdvancedSearch: false,
@ -16,57 +15,59 @@ const initialState = {
preset_refraction: null,
periphery_toricity: null,
side: 'all',
issued: false,
trial: null
issued: 'all',
trial: null,
},
};
const lensesSlice = createSlice({
name: 'lensesUI',
name: 'lenses',
initialState,
reducers: {
setSearchText: (state, action) => {
setSearchText(state, action) {
state.searchText = action.payload;
},
setCurrentPage: (state, action) => {
setSortOrder(state, action) {
state.sortOrder = action.payload;
},
setViewMode(state, action) {
state.viewMode = action.payload;
},
setCurrentPage(state, action) {
state.currentPage = action.payload;
},
setPageSize: (state, action) => {
setPageSize(state, action) {
state.pageSize = action.payload;
},
setViewMode: (state, action) => {
state.viewMode = action.payload;
cacheInfo("viewModeLenses", action.payload);
},
openModal: (state) => {
state.isModalVisible = true;
},
closeModal: (state) => {
state.isModalVisible = false;
state.selectedLens = null;
},
selectLens: (state, action) => {
selectLens(state, action) {
state.selectedLens = action.payload;
},
setSearchParams: (state, action) => {
state.searchParams = action.payload;
openModal(state) {
state.isModalVisible = true;
},
setShowAdvancedSearch: (state, action) => {
closeModal(state) {
state.isModalVisible = false;
},
setShowAdvancedSearch(state, action) {
state.showAdvancedSearch = action.payload;
},
}
setSearchParams(state, action) {
state.searchParams = action.payload;
},
},
});
export const {
setSearchText,
setSortOrder,
setViewMode,
setCurrentPage,
setPageSize,
setViewMode,
selectLens,
openModal,
closeModal,
selectLens,
setShowAdvancedSearch,
setSearchParams,
setShowAdvancedSearch
} = lensesSlice.actions;
export default lensesSlice.reducer;