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=
|
- *File*: =src/presentation/api/relay_api.rs=
|
||||||
- *Complexity*: Low | *Uncertainty*: Low
|
- *Complexity*: Low | *Uncertainty*: Low
|
||||||
|
|
||||||
*** TODO Frontend Implementation [0/2]
|
*** TODO Frontend Implementation [1/2]
|
||||||
- [ ] *T052* [P] [US1] [TDD] Create =RelayDto= TypeScript interface
|
- [X] *T052* [P] [US1] [TDD] Create =RelayDto= TypeScript interface
|
||||||
- Generate from OpenAPI spec or manually define
|
- Generate from OpenAPI spec or manually define
|
||||||
- *File*: =frontend/src/types/relay.ts=
|
- *File*: =frontend/src/types/relay.ts=
|
||||||
- *Complexity*: Low | *Uncertainty*: Low
|
- *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 { createApp } from 'vue';
|
||||||
import "./style.css";
|
|
||||||
import App from "./App.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,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": false,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,10 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": false,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user