Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd551a0de3 |
@@ -16,7 +16,7 @@ WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
COPY resources/ resources/
|
||||
COPY vite.config.js postcss.config.js tailwind.config.js ./
|
||||
COPY vite.config.js ./
|
||||
RUN npm run build
|
||||
|
||||
# --- final: php-fpm + nginx in one container, supervised ---
|
||||
|
||||
@@ -59,11 +59,11 @@ class DeviceController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function create()
|
||||
public function create(DeviceShadow $shadow)
|
||||
{
|
||||
$this->authorize('create', Device::class);
|
||||
|
||||
return Inertia::render('Devices/Create', $this->formOptions());
|
||||
return Inertia::render('Devices/Create', $this->formOptions($shadow));
|
||||
}
|
||||
|
||||
public function store(DeviceRequest $request)
|
||||
@@ -75,13 +75,13 @@ class DeviceController extends Controller
|
||||
return redirect()->route('devices.index')->with('status', 'Устройство добавлено.');
|
||||
}
|
||||
|
||||
public function edit(Device $device)
|
||||
public function edit(Device $device, DeviceShadow $shadow)
|
||||
{
|
||||
$this->authorize('update', $device);
|
||||
|
||||
return Inertia::render('Devices/Edit', [
|
||||
'device' => $device->only(['id', 'name', 'zone_id', 'device_type_id', 'external_id', 'protocol']),
|
||||
...$this->formOptions(),
|
||||
...$this->formOptions($shadow),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -150,11 +150,21 @@ class DeviceController extends Controller
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formOptions(): array
|
||||
private function formOptions(DeviceShadow $shadow): array
|
||||
{
|
||||
$registeredIds = Device::pluck('external_id')->all();
|
||||
$unregisteredExternalIds = collect($shadow->knownExternalIds())
|
||||
->diff($registeredIds)
|
||||
->sort()
|
||||
->values();
|
||||
|
||||
return [
|
||||
'zones' => ZoneResource::collection(Zone::orderBy('name')->get())->resolve(),
|
||||
'deviceTypes' => DeviceTypeResource::collection(DeviceType::orderBy('code')->get())->resolve(),
|
||||
// external_id of devices device-control-service has already seen
|
||||
// over MQTT but nobody has registered yet — offered as
|
||||
// suggestions (not a closed list) when adding/editing a device.
|
||||
'unregisteredExternalIds' => $unregisteredExternalIds,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,4 +54,18 @@ class DeviceShadow
|
||||
|
||||
return array_combine($externalIds, array_map(fn ($v) => $v ?: 'unknown', $values));
|
||||
}
|
||||
|
||||
/**
|
||||
* external_id of every device device-control-service has ever heard
|
||||
* from over MQTT (`devices:known`, a Redis Set it maintains) — includes
|
||||
* devices that sent telemetry/ack before anyone registered them in
|
||||
* Postgres. Used to suggest an external_id when adding a device instead
|
||||
* of requiring it to be typed exactly from memory.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function knownExternalIds(): array
|
||||
{
|
||||
return Redis::smembers('devices:known');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Form from '@/Pages/Devices/Form.vue';
|
||||
defineProps({
|
||||
zones: { type: Array, required: true },
|
||||
deviceTypes: { type: Array, required: true },
|
||||
unregisteredExternalIds: { type: Array, default: () => [] },
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -17,7 +18,7 @@ defineProps({
|
||||
<div class="py-12">
|
||||
<div class="max-w-xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
|
||||
<Form :zones="zones" :device-types="deviceTypes" />
|
||||
<Form :zones="zones" :device-types="deviceTypes" :unregistered-external-ids="unregisteredExternalIds" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ defineProps({
|
||||
device: { type: Object, required: true },
|
||||
zones: { type: Array, required: true },
|
||||
deviceTypes: { type: Array, required: true },
|
||||
unregisteredExternalIds: { type: Array, default: () => [] },
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -18,7 +19,7 @@ defineProps({
|
||||
<div class="py-12">
|
||||
<div class="max-w-xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg p-6">
|
||||
<Form :device="device" :zones="zones" :device-types="deviceTypes" />
|
||||
<Form :device="device" :zones="zones" :device-types="deviceTypes" :unregistered-external-ids="unregisteredExternalIds" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ const props = defineProps({
|
||||
device: { type: Object, default: null },
|
||||
zones: { type: Array, required: true },
|
||||
deviceTypes: { type: Array, required: true },
|
||||
unregisteredExternalIds: { type: Array, default: () => [] },
|
||||
});
|
||||
|
||||
const isEdit = !!props.device;
|
||||
@@ -76,9 +77,17 @@ function submit() {
|
||||
id="external_id"
|
||||
v-model="form.external_id"
|
||||
type="text"
|
||||
list="unregistered-external-ids"
|
||||
required
|
||||
placeholder="можно ввести вручную или выбрать увиденное устройство"
|
||||
class="mt-1 block w-full font-mono border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm"
|
||||
>
|
||||
<datalist id="unregistered-external-ids">
|
||||
<option v-for="id in unregisteredExternalIds" :key="id" :value="id" />
|
||||
</datalist>
|
||||
<p v-if="unregisteredExternalIds.length" class="mt-1 text-xs text-gray-400">
|
||||
От {{ unregisteredExternalIds.length }} {{ unregisteredExternalIds.length === 1 ? 'устройства' : 'устройств' }} уже приходил сигнал, но они ещё не зарегистрированы — начните вводить, чтобы увидеть подсказки.
|
||||
</p>
|
||||
<div v-if="form.errors.external_id" class="mt-2 text-sm text-red-600">{{ form.errors.external_id }}</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -130,4 +130,23 @@ class DeviceControllerTest extends TestCase
|
||||
->where('telemetry.0.value', 24.5)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_create_form_suggests_unregistered_external_ids(): void
|
||||
{
|
||||
$owner = User::factory()->create(['role' => UserRole::Owner]);
|
||||
// Already registered — must be excluded even though device-control-service still knows it.
|
||||
$this->makeDevice($owner, ['turn_on']);
|
||||
|
||||
$this->mock(DeviceShadow::class, function ($mock) {
|
||||
$mock->shouldReceive('knownExternalIds')->once()->andReturn(['fan-1', 'sensor-2']);
|
||||
});
|
||||
|
||||
$response = $this->actingAs($owner)->get(route('devices.create'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('Devices/Create')
|
||||
->where('unregisteredExternalIds', ['sensor-2'])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user