creae update devices

This commit is contained in:
Vasya Ryzhkoff 2024-07-23 01:43:13 +07:00
parent d9c3bfa35e
commit 65b924c174
23 changed files with 359 additions and 32 deletions

6
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/Yuna.iml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="DBE_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

12
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="@localhost" uuid="bfd16a45-57c4-4cdd-9553-7a07934e4c31">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Yuna.iml" filepath="$PROJECT_DIR$/.idea/Yuna.iml" />
</modules>
</component>
</project>

7
.idea/sqldialects.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/bfd16a45-57c4-4cdd-9553-7a07934e4c31/console_1.sql" dialect="PostgreSQL" />
<file url="PROJECT" dialect="PostgreSQL" />
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -80,5 +80,27 @@ namespace Yuna.Tests.Repositories
Assert.NotNull(result); Assert.NotNull(result);
Assert.True(result.All(x => x.UserId == 1)); Assert.True(result.All(x => x.UserId == 1));
} }
[Fact]
public async Task Update_Updates()
{
//Arrange
Settings.Init();
var _context = new DapperContext(true);
var repo = new DeviceRepository(_context);
//Act
var initial = await repo.GetById(1);
initial!.Name += "1";
var returnResult = await repo.Update(initial);
var realChanged = await repo.GetById(1);
//Assert
Assert.Equal(initial.Name, returnResult!.Name);
Assert.Equal(initial!.Name, realChanged!.Name);
}
} }
} }

View File

@ -16,9 +16,6 @@ namespace Yuna.Website.Server.API
app.MapPost("/api/device", CreateDevice) app.MapPost("/api/device", CreateDevice)
.WithTags("device"); .WithTags("device");
app.MapDelete("/api/device/{id:long}", () => { })
.WithTags("device");
app.MapGet("/api/device/{deviceId:long}", GetById) app.MapGet("/api/device/{deviceId:long}", GetById)
.WithTags("device"); .WithTags("device");
@ -32,6 +29,12 @@ namespace Yuna.Website.Server.API
app.MapPut("/api/device/{deviceId:long}", AddSkillsToDevice) app.MapPut("/api/device/{deviceId:long}", AddSkillsToDevice)
.WithTags("device"); .WithTags("device");
app.MapPut("/api/device", Update)
.WithTags("device");
app.MapDelete("/api/device/{deviceId:long}", Delete)
.WithTags("device");
} }
@ -99,10 +102,18 @@ namespace Yuna.Website.Server.API
} }
[Authorize] [Authorize]
public async Task<IResult> Delete(IDeviceService deviceService, long id) public async Task<IResult> Delete(IDeviceService deviceService, HttpContext context, long deviceId)
{ {
var result = await deviceService.Delete(id); var isAdmin = context.GetRoleFromCookie();
if (result is null) return Results.NotFound(); var userId = context.GetUserIdFromCookie();
var deviceToDelete = await deviceService.GetById(deviceId);
if(deviceToDelete is null) return Results.NotFound();
if (userId != deviceToDelete.UserId && !isAdmin) return Results.Forbid();
var result = await deviceService.Delete(deviceId);
if (result is null) return Results.Problem(statusCode: 500);
return Results.Ok(result); return Results.Ok(result);
} }
@ -129,5 +140,40 @@ namespace Yuna.Website.Server.API
return Results.Ok(result); return Results.Ok(result);
} }
public class UpdateDeviceRequest
{
[JsonPropertyName("id")]
public long Id { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; } = null!;
[JsonPropertyName("description")]
public string Description { get; set; } = "";
[JsonPropertyName("deviceUrl")]
public string DeviceUrl { get; set; } = null!;
}
[Authorize]
public async Task<IResult> Update([FromBody] UpdateDeviceRequest request, HttpContext context, IDeviceService deviceService)
{
var userId = context.GetUserIdFromCookie();
var isAdmin = context.GetRoleFromCookie();
var device = await deviceService.GetById(request.Id);
if (device is null) return Results.NotFound();
if (device.UserId != userId && !isAdmin) return Results.Forbid();
device.DeviceUrl = request.DeviceUrl;
device.Name = request.Name;
device.Description = request.Description;
var result = await deviceService.Update(device);
return Results.Ok(result);
}
} }
} }

View File

@ -36,6 +36,11 @@ namespace Yuna.Website.Server.Infrastructure
private static void LoadConnectionStrs(JsonElement connectionStrs) private static void LoadConnectionStrs(JsonElement connectionStrs)
{ {
var env = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
{
}
DbConnectionStr = connectionStrs.GetProperty("Db").GetString()!; DbConnectionStr = connectionStrs.GetProperty("Db").GetString()!;
} }

View File

@ -107,9 +107,6 @@ namespace Yuna.Website.Server.Services.DeviceService
public async Task<Device?> Delete(long id) public async Task<Device?> Delete(long id)
{ {
var prop = await _deviceRepository.GetById(id);
if (prop is null) return null;
var result = await _deviceRepository.Delete(id); var result = await _deviceRepository.Delete(id);
return result; return result;
} }
@ -130,5 +127,11 @@ namespace Yuna.Website.Server.Services.DeviceService
var result = await _deviceRepository.GetList(userId); var result = await _deviceRepository.GetList(userId);
return result ?? []; return result ?? [];
} }
public async Task<Device?> Update(Device device)
{
var result = await _deviceRepository.Update(device);
return result;
}
} }
} }

View File

@ -12,5 +12,6 @@ namespace Yuna.Website.Server.Services.DeviceService
public Task<Device?> Delete(long id); public Task<Device?> Delete(long id);
public Task<Device?> AddProps(IReadOnlyList<Prop> props, long deviceId); public Task<Device?> AddProps(IReadOnlyList<Prop> props, long deviceId);
public Task<Dictionary<long, string>?> FetchPropsData(Device device); public Task<Dictionary<long, string>?> FetchPropsData(Device device);
public Task<Device?> Update(Device device);
} }
} }

View File

@ -1,5 +1,6 @@
using Dapper; using Dapper;
using System.Data; using System.Data;
using Yuna.Website.Server.Model;
namespace Yuna.Website.Server.Storage.Repositories.Device namespace Yuna.Website.Server.Storage.Repositories.Device
{ {
@ -36,9 +37,18 @@ namespace Yuna.Website.Server.Storage.Repositories.Device
return result; return result;
} }
public Task<Model.Device?> Delete(long id) public async Task<Model.Device?> Delete(long id)
{ {
throw new NotImplementedException(); var query =
$@"
UPDATE ""Yuna_Devices""
SET ""IsDeleted"" = TRUE
WHERE ""Id"" = {id}
AND NOT ""IsDeleted""
returning *
";
return await _context.Connection.QuerySingleOrDefaultAsync<Model.Device>(query);
} }
public async Task<Model.Device?> GetById(long id) public async Task<Model.Device?> GetById(long id)
@ -58,7 +68,8 @@ namespace Yuna.Website.Server.Storage.Repositories.Device
FROM ""Yuna_Devices"" d FROM ""Yuna_Devices"" d
LEFT JOIN ""Yuna_Props_In_Devices"" pd ON d.""Id"" = pd.""DeviceId"" LEFT JOIN ""Yuna_Props_In_Devices"" pd ON d.""Id"" = pd.""DeviceId""
LEFT JOIN ""Yuna_Props"" p ON pd.""PropId"" = p.""Id"" LEFT JOIN ""Yuna_Props"" p ON pd.""PropId"" = p.""Id""
WHERE d.""Id"" = {id}"; WHERE d.""Id"" = {id}
AND NOT d.""IsDeleted""";
var deviceDict = new Dictionary<long, Model.Device>(); var deviceDict = new Dictionary<long, Model.Device>();
var result = await _context.Connection.QueryAsync<Model.Device, Model.Prop, Model.Device>( var result = await _context.Connection.QueryAsync<Model.Device, Model.Prop, Model.Device>(
@ -96,7 +107,9 @@ namespace Yuna.Website.Server.Storage.Repositories.Device
p.""Type"" as {nameof(Model.Prop.Type)} p.""Type"" as {nameof(Model.Prop.Type)}
FROM ""Yuna_Devices"" d FROM ""Yuna_Devices"" d
LEFT JOIN ""Yuna_Props_In_Devices"" pd ON d.""Id"" = pd.""DeviceId"" LEFT JOIN ""Yuna_Props_In_Devices"" pd ON d.""Id"" = pd.""DeviceId""
LEFT JOIN ""Yuna_Props"" p ON pd.""PropId"" = p.""Id"""; LEFT JOIN ""Yuna_Props"" p ON pd.""PropId"" = p.""Id""
WHERE NOT d.""IsDeleted""
ORDER BY d.""Id""";
var deviceDict = new Dictionary<long, Model.Device>(); var deviceDict = new Dictionary<long, Model.Device>();
var result = await _context.Connection.QueryAsync<Model.Device, Model.Prop, Model.Device>( var result = await _context.Connection.QueryAsync<Model.Device, Model.Prop, Model.Device>(
@ -135,7 +148,9 @@ namespace Yuna.Website.Server.Storage.Repositories.Device
FROM ""Yuna_Devices"" d FROM ""Yuna_Devices"" d
LEFT JOIN ""Yuna_Props_In_Devices"" pd ON d.""Id"" = pd.""DeviceId"" LEFT JOIN ""Yuna_Props_In_Devices"" pd ON d.""Id"" = pd.""DeviceId""
LEFT JOIN ""Yuna_Props"" p ON pd.""PropId"" = p.""Id"" LEFT JOIN ""Yuna_Props"" p ON pd.""PropId"" = p.""Id""
WHERE d.""UserId"" = {userId};"; WHERE d.""UserId"" = {userId}
AND NOT d.""IsDeleted""
ORDER BY d.""Id""";
var deviceDict = new Dictionary<long, Model.Device>(); var deviceDict = new Dictionary<long, Model.Device>();
var result = await _context.Connection.QueryAsync<Model.Device, Model.Prop, Model.Device>( var result = await _context.Connection.QueryAsync<Model.Device, Model.Prop, Model.Device>(
@ -156,5 +171,22 @@ namespace Yuna.Website.Server.Storage.Repositories.Device
return result.Distinct().ToList(); return result.Distinct().ToList();
} }
public async Task<Model.Device?> Update(Model.Device device)
{
var query =
$@"
UPDATE public.""Yuna_Devices""
SET ""Name"" = @Name,
""Description"" = @Description,
""DeviceUrl"" = @DeviceUrl,
""UserId"" = @UserId
WHERE ""Id"" = @Id
AND NOT ""IsDeleted""
returning *
";
return await _context.Connection.QuerySingleOrDefaultAsync<Model.Device>(query, device);
}
} }
} }

View File

@ -8,7 +8,7 @@ namespace Yuna.Website.Server.Storage.Repositories.Device
public Task<IReadOnlyList<Model.Device>> GetList(); public Task<IReadOnlyList<Model.Device>> GetList();
public Task<IReadOnlyList<Model.Device>> GetList(long userId); public Task<IReadOnlyList<Model.Device>> GetList(long userId);
public Task<Model.Device?> Create(Model.Device device); public Task<Model.Device?> Create(Model.Device device);
//public Task<User?> Update(User user); public Task<Model.Device?> Update(Model.Device device);
public Task<Model.Device?> Delete(long id); public Task<Model.Device?> Delete(long id);
public Task AddProps(IReadOnlyList<Model.Prop> skills, long deviceId); public Task AddProps(IReadOnlyList<Model.Prop> skills, long deviceId);
} }

View File

@ -1,11 +1,14 @@
import { Header } from "../../components/Header" import { Header } from "../../components/Header"
import { DeviceList } from "./components/DeviceList"; import { DeviceList } from "./components/DeviceList";
import { EditModal } from "./components/modals/EditModal";
export const HomePage = () => { export const HomePage = () => {
return ( return (
<> <>
<Header /> <Header />
<DeviceList /> <DeviceList />
<EditModal />
</> </>
); );
} }

View File

@ -2,8 +2,10 @@ import { useEffect, useMemo } from "react";
import "../../../resources/styles/home.scss" import "../../../resources/styles/home.scss"
import { IDeviceDto, IPropDto } from "../types"; import { IDeviceDto, IPropDto } from "../types";
import { DeviceCardStore } from "../model/DeviceCardStore"; import { DeviceCardStore } from "../model/DeviceCardStore";
import { Button, Center, Skeleton, useInterval } from "@chakra-ui/react"; import { Button, Center, IconButton, Skeleton, Spacer, useInterval } from "@chakra-ui/react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { MdOutlineDelete, MdOutlineSettings } from "react-icons/md";
import { homePageStore } from "../model/HomePageStore";
@ -36,11 +38,25 @@ export const DeviceCard = observer((props: { data: IDeviceDto }) => {
<div className="device_card"> <div className="device_card">
<div className="h1_container"> <div className="h1_container">
<h1>{props.data.name}</h1> <h1>{props.data.name}</h1>
<Button>sdfs</Button> <div className="button_container">
<IconButton
colorScheme="red"
onClick={() => homePageStore.deleteDevice(props.data.id)}
icon={<MdOutlineDelete />}
aria-label={"delete"}
fontSize={"90%"}
size={'sm'} />
<IconButton
onClick={() => homePageStore.setEditModalOpened(props.data.id, true)}
icon={<MdOutlineSettings />}
aria-label={"edit"}
fontSize={"90%"}
size={'sm'} />
</div>
</div> </div>
<p style={{ textAlign: 'center' }}>{props.data.description}</p> <p style={{ textAlign: 'center' }}>{props.data.description}</p>
<ul> <ul>
{getPropsList(props.data.props, deviceCardStore.isLoading)} {getPropsList(props.data.props ?? [], deviceCardStore.isLoading)}
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -0,0 +1,71 @@
import { observer } from "mobx-react";
import { homePageStore } from "../../model/HomePageStore";
import { Button, Center, Input, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, Stack, Textarea } from "@chakra-ui/react";
import { IDeviceDto } from "../../types";
import { useEffect, useState } from "react";
export const EditModal = observer(() => {
const currentDevice = homePageStore.currentEditDevice
const [name, setName] = useState(currentDevice?.name);
const [description, setDescription] = useState(currentDevice?.description);
const [url, setUrl] = useState(currentDevice?.deviceUrl);
useEffect(
() => {
setName(currentDevice?.name);
setDescription(currentDevice?.description);
setUrl(currentDevice?.deviceUrl)
},
[currentDevice]
)
const onUpdateDevice = () => {
homePageStore.setEditModalOpened(currentDevice!.id, false);
homePageStore.updateDevice({
id: currentDevice?.id,
name: name,
description: description,
deviceUrl: url
} as IDeviceDto);
}
return (<Modal
onClose={() => homePageStore.setEditModalOpened(homePageStore.editModalCurrentId!, false)}
isOpen={homePageStore.isEditModalOpened}
isCentered
blockScrollOnMount={false}
>
<ModalOverlay />
<ModalContent>
<ModalHeader>Редактировать</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Stack spacing={3}>
<label>Название:</label>
<Input
value={name ?? ""}
onChange={e => setName(e.target.value)}
disabled={homePageStore.isLoading} />
<label>Описание:</label>
<Textarea
value={description ?? ""}
onChange={e => setDescription(e.target.value)}
disabled={homePageStore.isLoading} />
<label>URL:</label>
<Input
value={url ?? ""}
onChange={e => setUrl(e.target.value)}
disabled={homePageStore.isLoading} />
</Stack>
<Center>
<Button
colorScheme="green"
marginTop={10}
onClick={() => onUpdateDevice()}
isLoading={homePageStore.isLoading}>Сохранить</Button>
</Center>
</ModalBody>
</ModalContent>
</Modal>);
});

View File

@ -1,15 +1,29 @@
import { makeAutoObservable } from "mobx"; import { makeAutoObservable } from "mobx";
import { IDeviceDto } from "../types"; import { IDeviceDto } from "../types";
import { HomePageService } from "../services/HomePageService"; import { HomePageService } from "../services/HomePageService";
import { error, info } from "../../../utils/ToastHelper";
class HomePageStore { class HomePageStore {
devices: IDeviceDto[] = [] devices: IDeviceDto[] = []
isLoading: boolean = false; isLoading: boolean = false;
isEditModalOpened: boolean = false;
editModalCurrentId: number | null = null;
currentEditDevice: IDeviceDto | undefined;
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);
} }
setEditModalOpened(id: number, isOpened: boolean): void {
this.currentEditDevice = { ...this.devices.find(device => device.id === id) } as IDeviceDto;
this.isEditModalOpened = isOpened;
}
setCurrentEditDevice(value: IDeviceDto | undefined) {
this.currentEditDevice = value;
}
setDevices(value: IDeviceDto[]) { setDevices(value: IDeviceDto[]) {
this.devices = value; this.devices = value;
@ -21,6 +35,29 @@ class HomePageStore {
this.setDevices(result); this.setDevices(result);
this.isLoading = false; this.isLoading = false;
} }
async updateDevice(value: IDeviceDto) {
const result = await HomePageService.updateDevice(value);
if (result === null) {
error("Ошибка", "Запрос не был выполнен");
return;
}
this.setCurrentEditDevice(undefined);
info("Успешно", "Изменения были сохранены")
await this.loadDevices();
}
async deleteDevice(value: number) {
const result = await HomePageService.deleteDevice(value);
if (result === null) {
error("Ошибка", "Запрос не был выполнен");
return;
}
info("Успешно", "Устройство было удалено");
await this.loadDevices();
}
} }
export const homePageStore = new HomePageStore(); export const homePageStore = new HomePageStore();

View File

@ -21,4 +21,30 @@ export class HomePageService {
if (response.status === 200) return response.data; if (response.status === 200) return response.data;
return null; return null;
} }
public static async updateDevice(dto: IDeviceDto): Promise<IDeviceDto | null> {
try {
const response = await api.put<IDeviceDto>(`device`, dto);
if (response.status === 200) return response.data;
}
catch (error) {
console.error("Error: ", error);
}
return null;
}
public static async deleteDevice(id: number): Promise<IDeviceDto | null> {
try {
const response = await api.delete<IDeviceDto>(`device/${id}`);
if (response.status === 200) return response.data;
}
catch (error) {
console.error("Error: ", error);
}
return null;
}
} }

View File

@ -1,9 +1,9 @@
export interface IDeviceDto { export interface IDeviceDto {
id: number; id: number;
name: string; name: string;
description: string; description?: string;
props: IPropDto[] props?: IPropDto[]
deviceUrl: string; deviceUrl?: string;
} }
export enum propType { export enum propType {

View File

@ -6,6 +6,7 @@
width: 80%; width: 80%;
height: 70px; height: 70px;
box-shadow: 5px 5px 0px #319795; box-shadow: 5px 5px 0px #319795;
z-index: 1;
/* x-offset y-offset blur-radius color */ /* x-offset y-offset blur-radius color */
h1 { h1 {

View File

@ -15,19 +15,26 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 0px 50px; padding: 0px 0px;
.h1_container { .h1_container {
overflow: hidden;
height: 80px;
h1 {}
button {
position: relative; position: relative;
top: -50px; overflow: visible;
width: 10px; width: 100%;
float: right;
h1 {
max-width: 450px;
display: block;
margin: auto;
}
.button_container {
display: flex;
flex-direction: row;
gap: 5px;
position: absolute;
top: 25px;
right: 30px;
} }
} }

11
package-lock.json generated
View File

@ -5,7 +5,8 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"mobx-react": "^9.1.1" "mobx-react": "^9.1.1",
"react-icons": "^5.2.1"
} }
}, },
"node_modules/js-tokens": { "node_modules/js-tokens": {
@ -96,6 +97,14 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-icons": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
"integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==",
"peerDependencies": {
"react": "*"
}
},
"node_modules/use-sync-external-store": { "node_modules/use-sync-external-store": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",

View File

@ -1,5 +1,6 @@
{ {
"dependencies": { "dependencies": {
"mobx-react": "^9.1.1" "mobx-react": "^9.1.1",
"react-icons": "^5.2.1"
} }
} }