feat: add relay composables
This commit is contained in:
@@ -891,8 +891,8 @@ CLOSED: [2026-05-14 jeu. 20:09]
|
||||
- *File*: =src/presentation/api/relay_api.rs=
|
||||
- *Complexity*: Low | *Uncertainty*: Low
|
||||
|
||||
*** TODO Frontend Implementation [0/2]
|
||||
- [ ] *T052* [P] [US1] [TDD] Create =RelayDto= TypeScript interface
|
||||
*** TODO Frontend Implementation [1/2]
|
||||
- [X] *T052* [P] [US1] [TDD] Create =RelayDto= TypeScript interface
|
||||
- Generate from OpenAPI spec or manually define
|
||||
- *File*: =frontend/src/types/relay.ts=
|
||||
- *Complexity*: Low | *Uncertainty*: Low
|
||||
|
||||
37
src/composables/useRelay.ts
Normal file
37
src/composables/useRelay.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { ref } from 'vue';
|
||||
import { apiClient } from '../api/client';
|
||||
import { relayDtoToDomain } from '../types/mappers/relayDtoMapper';
|
||||
import type { Relay, RelayDto } from '../types/relay';
|
||||
|
||||
export function useRelay() {
|
||||
const isLoading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
const response = ref<Relay | null>(null);
|
||||
|
||||
const toggleRelay = async (id: number) => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const { data } = await apiClient.POST('/api/relays/{id}/toggle', {
|
||||
params: {
|
||||
path: {
|
||||
id,
|
||||
},
|
||||
},
|
||||
});
|
||||
error.value = null;
|
||||
response.value = relayDtoToDomain(data as RelayDto);
|
||||
} catch (err: any) {
|
||||
console.error(`Failed to toggle relay ${id}:`, err);
|
||||
error.value = err.message || `Failed to toggle relay ${id}`;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
toggleRelay,
|
||||
isLoading,
|
||||
error,
|
||||
response,
|
||||
};
|
||||
}
|
||||
51
src/composables/useRelayPolling.ts
Normal file
51
src/composables/useRelayPolling.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { apiClient } from '../api/client';
|
||||
import { relayDtoToDomain } from '../types/mappers/relayDtoMapper';
|
||||
import type { Relay } from '../types/relay';
|
||||
import { isNil } from '../utils/isNil';
|
||||
|
||||
export function useRelayPolling(intervalMs: number = 2000) {
|
||||
const relays = ref<Relay[]>([]);
|
||||
const isLoading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
let pollingInterval: number | null = null;
|
||||
|
||||
const fetchData = async () => {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
const { data } = await apiClient.GET('/api/relays');
|
||||
relays.value = data?.map(relayDtoToDomain) ?? [];
|
||||
error.value = null;
|
||||
} catch (err: any) {
|
||||
console.error('Polling error:', err);
|
||||
error.value = err.message || 'Failed to fetch data';
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const startPolling = () => {
|
||||
fetchData();
|
||||
pollingInterval = window.setInterval(fetchData, intervalMs);
|
||||
};
|
||||
|
||||
const stopPolling = () => {
|
||||
if (isNil(pollingInterval)) {
|
||||
return;
|
||||
}
|
||||
clearInterval(pollingInterval);
|
||||
pollingInterval = null;
|
||||
};
|
||||
|
||||
onMounted(startPolling);
|
||||
onUnmounted(stopPolling);
|
||||
|
||||
return {
|
||||
relays,
|
||||
isLoading,
|
||||
error,
|
||||
refresh: fetchData,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createApp } from "vue";
|
||||
import "./style.css";
|
||||
import App from "./App.vue";
|
||||
import { createApp } from 'vue';
|
||||
|
||||
createApp(App).mount("#app");
|
||||
import './style.css';
|
||||
import App from './App.vue';
|
||||
|
||||
createApp(App).mount('#app');
|
||||
|
||||
13
src/types/mappers/relayDtoMapper.ts
Normal file
13
src/types/mappers/relayDtoMapper.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { isNil } from '../../utils/isNil';
|
||||
import { RelayState, Relay, type RelayDto } from '../relay';
|
||||
|
||||
const relayStateToDomain = (dto: string | null): RelayState => {
|
||||
if (isNil(dto) || dto.trim() === '') {
|
||||
return RelayState.Off;
|
||||
}
|
||||
return dto.trim().toLowerCase() === 'on' ? RelayState.On : RelayState.Off;
|
||||
};
|
||||
|
||||
export const relayDtoToDomain = (dto: RelayDto): Relay => {
|
||||
return new Relay(dto.id, relayStateToDomain(dto.state), dto.label);
|
||||
};
|
||||
20
src/types/relay.ts
Normal file
20
src/types/relay.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { components } from '../api/schema';
|
||||
|
||||
export type RelayDto = components['schemas']['RelayDto'];
|
||||
|
||||
export enum RelayState {
|
||||
On = 'on',
|
||||
Off = 'off',
|
||||
}
|
||||
|
||||
export class Relay {
|
||||
id: number;
|
||||
state: RelayState;
|
||||
label: string;
|
||||
|
||||
constructor(id: number, state: RelayState, label: string) {
|
||||
this.id = id;
|
||||
this.state = state;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
2
src/utils/isNil.ts
Normal file
2
src/utils/isNil.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const isNil = (value: unknown | null | undefined): value is null | undefined =>
|
||||
value === null || value === undefined;
|
||||
@@ -8,9 +8,10 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"erasableSyntaxOnly": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
|
||||
@@ -18,9 +18,10 @@
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"erasableSyntaxOnly": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user