Panels become devices: per-device documents in the flow repo, a Devices tab
- flow/devices.py: DeviceDef in FLOWS_DIR/_devices/<id>.json, committed under
the flow store's write lock, 409 on a stale version; nonces in DEVICES_FILE
outside git; in-memory presence announced as device_status
- api/routes/devices.py: CRUD, pairing with an optional {name, board,
firmware}, a pending list adopted without a code from the LAN only, unpair;
/panels/pair, /panels/pair/{code} and GET /panels/{id} kept as aliases,
PUT /panels/ gone
- _device_may replaces _panel_may, JWT claims unchanged
- panels.json moves into devices once at startup, then .migrated
- MCP list_devices, get_device; search category device
- Devices tab (list, detail with presence, pairing, ordered dashboards,
touch, I/O), PanelsDialog removed, serialised optimistic saves
This commit is contained in:
+248
-131
@@ -911,6 +911,179 @@ export const DeadLetterSchema = {
|
||||
title: 'DeadLetter'
|
||||
} as const;
|
||||
|
||||
export const DeviceCreateSchema = {
|
||||
properties: {
|
||||
title: {
|
||||
type: 'string',
|
||||
title: 'Title',
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
title: 'DeviceCreate'
|
||||
} as const;
|
||||
|
||||
export const DeviceDefSchema = {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
title: 'Id'
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
title: 'Title',
|
||||
default: ''
|
||||
},
|
||||
dashboards: {
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Dashboards'
|
||||
},
|
||||
touch: {
|
||||
type: 'boolean',
|
||||
title: 'Touch',
|
||||
default: false
|
||||
},
|
||||
io: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/DevicePort'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Io'
|
||||
},
|
||||
version: {
|
||||
type: 'integer',
|
||||
title: 'Version',
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
required: ['id'],
|
||||
title: 'DeviceDef',
|
||||
description: 'One device, and what it shows.'
|
||||
} as const;
|
||||
|
||||
export const DevicePortSchema = {
|
||||
properties: {
|
||||
port: {
|
||||
type: 'string',
|
||||
title: 'Port'
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
title: 'Message'
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
enum: ['in', 'out'],
|
||||
title: 'Direction'
|
||||
},
|
||||
config: {
|
||||
additionalProperties: true,
|
||||
type: 'object',
|
||||
title: 'Config'
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
required: ['port', 'message', 'direction'],
|
||||
title: 'DevicePort',
|
||||
description: `One piece of the device's own hardware, wired to a message.
|
||||
|
||||
A wall panel is a node as well as a screen: a button on its GPIO, a
|
||||
microphone, a speaker. Which ports exist is the device's business, so the
|
||||
name is free text and \`\`config\`\` is passed through to it unread.`
|
||||
} as const;
|
||||
|
||||
export const DevicePublicSchema = {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
title: 'Id'
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
title: 'Title',
|
||||
default: ''
|
||||
},
|
||||
dashboards: {
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Dashboards'
|
||||
},
|
||||
touch: {
|
||||
type: 'boolean',
|
||||
title: 'Touch',
|
||||
default: false
|
||||
},
|
||||
io: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/DevicePort'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Io'
|
||||
},
|
||||
version: {
|
||||
type: 'integer',
|
||||
title: 'Version',
|
||||
default: 0
|
||||
},
|
||||
online: {
|
||||
type: 'boolean',
|
||||
title: 'Online',
|
||||
default: false
|
||||
},
|
||||
last_seen: {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string',
|
||||
format: 'date-time'
|
||||
},
|
||||
{
|
||||
type: 'null'
|
||||
}
|
||||
],
|
||||
title: 'Last Seen'
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
required: ['id'],
|
||||
title: 'DevicePublic',
|
||||
description: 'A device as the Devices tab lists it, with whether it is there.'
|
||||
} as const;
|
||||
|
||||
export const DevicesPublicSchema = {
|
||||
properties: {
|
||||
data: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/DevicePublic'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Data'
|
||||
},
|
||||
count: {
|
||||
type: 'integer',
|
||||
title: 'Count',
|
||||
default: 0
|
||||
},
|
||||
frontend_host: {
|
||||
type: 'string',
|
||||
title: 'Frontend Host',
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
title: 'DevicesPublic',
|
||||
description: `The devices, and the address a device should be pointed at.
|
||||
|
||||
The address is the server's own, because the browser's origin is not a
|
||||
reliable answer: an admin working through the portal is on the portal's
|
||||
origin, and this one is for a screen on this network.`
|
||||
} as const;
|
||||
|
||||
export const EndpointSchema = {
|
||||
properties: {
|
||||
kind: {
|
||||
@@ -2924,16 +3097,45 @@ export const OAuthClientRegisterSchema = {
|
||||
description: 'RFC 7591 dynamic client registration request.'
|
||||
} as const;
|
||||
|
||||
export const PairHelloSchema = {
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
title: 'Name',
|
||||
default: ''
|
||||
},
|
||||
board: {
|
||||
type: 'string',
|
||||
title: 'Board',
|
||||
default: ''
|
||||
},
|
||||
firmware: {
|
||||
type: 'string',
|
||||
title: 'Firmware',
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
title: 'PairHello',
|
||||
description: 'What a device may say about itself when it asks for a code.'
|
||||
} as const;
|
||||
|
||||
export const PairRequestSchema = {
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
title: 'Code'
|
||||
title: 'Code',
|
||||
default: ''
|
||||
},
|
||||
request: {
|
||||
type: 'string',
|
||||
title: 'Request',
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
required: ['code'],
|
||||
title: 'PairRequest'
|
||||
title: 'PairRequest',
|
||||
description: 'Adopt the device showing ``code``, or the pending ``request``.'
|
||||
} as const;
|
||||
|
||||
export const PairStartedSchema = {
|
||||
@@ -2971,6 +3173,17 @@ export const PairStatusSchema = {
|
||||
],
|
||||
title: 'Access Token'
|
||||
},
|
||||
device: {
|
||||
anyOf: [
|
||||
{
|
||||
type: 'string'
|
||||
},
|
||||
{
|
||||
type: 'null'
|
||||
}
|
||||
],
|
||||
title: 'Device'
|
||||
},
|
||||
panel: {
|
||||
anyOf: [
|
||||
{
|
||||
@@ -2985,145 +3198,49 @@ export const PairStatusSchema = {
|
||||
},
|
||||
type: 'object',
|
||||
title: 'PairStatus',
|
||||
description: 'Nothing yet, or the credential and the panel it is for.'
|
||||
} as const;
|
||||
|
||||
export const PanelDefSchema = {
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
title: 'Id'
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
title: 'Title',
|
||||
default: ''
|
||||
},
|
||||
dashboards: {
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Dashboards'
|
||||
},
|
||||
touch: {
|
||||
type: 'boolean',
|
||||
title: 'Touch',
|
||||
default: false
|
||||
},
|
||||
io: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/PanelPort'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Io'
|
||||
},
|
||||
nonce: {
|
||||
type: 'integer',
|
||||
title: 'Nonce',
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
required: ['id'],
|
||||
title: 'PanelDef',
|
||||
description: 'One device, and what it shows.'
|
||||
} as const;
|
||||
|
||||
export const PanelPortSchema = {
|
||||
properties: {
|
||||
port: {
|
||||
type: 'string',
|
||||
title: 'Port'
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
title: 'Message'
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
enum: ['in', 'out'],
|
||||
title: 'Direction'
|
||||
},
|
||||
config: {
|
||||
additionalProperties: true,
|
||||
type: 'object',
|
||||
title: 'Config'
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
required: ['port', 'message', 'direction'],
|
||||
title: 'PanelPort',
|
||||
description: `One piece of the device's own hardware, wired to a message.
|
||||
|
||||
A wall panel is a node as well as a screen: a button on its GPIO, a
|
||||
microphone, a speaker. Which ports exist is the device's business, so the
|
||||
name is free text and \`\`config\`\` is passed through to it unread.`
|
||||
} as const;
|
||||
|
||||
export const PanelsConfigSchema = {
|
||||
properties: {
|
||||
panels: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/PanelDef'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Panels'
|
||||
},
|
||||
version: {
|
||||
type: 'integer',
|
||||
title: 'Version',
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
title: 'PanelsConfig',
|
||||
description: 'Every panel this instance knows about.'
|
||||
} as const;
|
||||
|
||||
export const PanelsPublicSchema = {
|
||||
properties: {
|
||||
panels: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/PanelDef'
|
||||
},
|
||||
type: 'array',
|
||||
title: 'Panels'
|
||||
},
|
||||
version: {
|
||||
type: 'integer',
|
||||
title: 'Version',
|
||||
default: 0
|
||||
},
|
||||
frontend_host: {
|
||||
type: 'string',
|
||||
title: 'Frontend Host',
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
title: 'PanelsPublic',
|
||||
description: `The panels, and the address a device should be pointed at.
|
||||
|
||||
The address is the server's own, because the browser's origin is not a
|
||||
reliable answer to it: an admin working through the portal is on the
|
||||
portal's origin, and this one is for a screen on this network.
|
||||
|
||||
An instance enrolled with a portal has a second address, built by the
|
||||
dialog from what \`\`/cloud/status\`\` reports rather than from here — a panel
|
||||
is not the thing that knows whether remote access is on.`
|
||||
description: 'Nothing yet, or the credential and the device it is for.'
|
||||
} as const;
|
||||
|
||||
export const PendingDeviceSchema = {
|
||||
properties: {
|
||||
request: {
|
||||
type: 'string',
|
||||
title: 'Request',
|
||||
default: ''
|
||||
},
|
||||
device: {
|
||||
type: 'string',
|
||||
title: 'Device'
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
title: 'Name',
|
||||
default: ''
|
||||
},
|
||||
board: {
|
||||
type: 'string',
|
||||
title: 'Board',
|
||||
default: ''
|
||||
},
|
||||
firmware: {
|
||||
type: 'string',
|
||||
title: 'Firmware',
|
||||
default: ''
|
||||
},
|
||||
remote: {
|
||||
type: 'boolean',
|
||||
title: 'Remote',
|
||||
default: false
|
||||
},
|
||||
needs_code: {
|
||||
type: 'boolean',
|
||||
title: 'Needs Code',
|
||||
default: true
|
||||
},
|
||||
expires_in: {
|
||||
type: 'integer',
|
||||
title: 'Expires In',
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
type: 'object',
|
||||
@@ -3772,7 +3889,7 @@ export const SearchEntrySchema = {
|
||||
properties: {
|
||||
category: {
|
||||
type: 'string',
|
||||
enum: ['flow', 'node', 'input', 'output', 'dashboard', 'widget', 'panel', 'secret', 'module', 'worker', 'alert'],
|
||||
enum: ['flow', 'node', 'input', 'output', 'dashboard', 'widget', 'device', 'secret', 'module', 'worker', 'alert'],
|
||||
title: 'Category'
|
||||
},
|
||||
name: {
|
||||
|
||||
+254
-200
File diff suppressed because one or more lines are too long
+147
-101
@@ -229,6 +229,67 @@ export type DeadLetter = {
|
||||
reason: string;
|
||||
};
|
||||
|
||||
export type DeviceCreate = {
|
||||
title?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* One device, and what it shows.
|
||||
*/
|
||||
export type DeviceDef = {
|
||||
id: string;
|
||||
title?: string;
|
||||
dashboards?: Array<(string)>;
|
||||
touch?: boolean;
|
||||
io?: Array<DevicePort>;
|
||||
version?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* One piece of the device's own hardware, wired to a message.
|
||||
*
|
||||
* A wall panel is a node as well as a screen: a button on its GPIO, a
|
||||
* microphone, a speaker. Which ports exist is the device's business, so the
|
||||
* name is free text and ``config`` is passed through to it unread.
|
||||
*/
|
||||
export type DevicePort = {
|
||||
port: string;
|
||||
message: string;
|
||||
direction: 'in' | 'out';
|
||||
config?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export type direction = 'in' | 'out';
|
||||
|
||||
/**
|
||||
* A device as the Devices tab lists it, with whether it is there.
|
||||
*/
|
||||
export type DevicePublic = {
|
||||
id: string;
|
||||
title?: string;
|
||||
dashboards?: Array<(string)>;
|
||||
touch?: boolean;
|
||||
io?: Array<DevicePort>;
|
||||
version?: number;
|
||||
online?: boolean;
|
||||
last_seen?: (string | null);
|
||||
};
|
||||
|
||||
/**
|
||||
* The devices, and the address a device should be pointed at.
|
||||
*
|
||||
* The address is the server's own, because the browser's origin is not a
|
||||
* reliable answer: an admin working through the portal is on the portal's
|
||||
* origin, and this one is for a screen on this network.
|
||||
*/
|
||||
export type DevicesPublic = {
|
||||
data?: Array<DevicePublic>;
|
||||
count?: number;
|
||||
frontend_host?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializable payload types.
|
||||
*
|
||||
@@ -982,8 +1043,21 @@ export type OAuthClientRegister = {
|
||||
token_endpoint_auth_method?: (string | null);
|
||||
};
|
||||
|
||||
/**
|
||||
* What a device may say about itself when it asks for a code.
|
||||
*/
|
||||
export type PairHello = {
|
||||
name?: string;
|
||||
board?: string;
|
||||
firmware?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adopt the device showing ``code``, or the pending ``request``.
|
||||
*/
|
||||
export type PairRequest = {
|
||||
code: string;
|
||||
code?: string;
|
||||
request?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -996,74 +1070,26 @@ export type PairStarted = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Nothing yet, or the credential and the panel it is for.
|
||||
* Nothing yet, or the credential and the device it is for.
|
||||
*/
|
||||
export type PairStatus = {
|
||||
access_token?: (string | null);
|
||||
device?: (string | null);
|
||||
panel?: (string | null);
|
||||
};
|
||||
|
||||
/**
|
||||
* One device, and what it shows.
|
||||
*/
|
||||
export type PanelDef = {
|
||||
id: string;
|
||||
title?: string;
|
||||
dashboards?: Array<(string)>;
|
||||
touch?: boolean;
|
||||
io?: Array<PanelPort>;
|
||||
nonce?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* One piece of the device's own hardware, wired to a message.
|
||||
*
|
||||
* A wall panel is a node as well as a screen: a button on its GPIO, a
|
||||
* microphone, a speaker. Which ports exist is the device's business, so the
|
||||
* name is free text and ``config`` is passed through to it unread.
|
||||
*/
|
||||
export type PanelPort = {
|
||||
port: string;
|
||||
message: string;
|
||||
direction: 'in' | 'out';
|
||||
config?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export type direction = 'in' | 'out';
|
||||
|
||||
/**
|
||||
* Every panel this instance knows about.
|
||||
*/
|
||||
export type PanelsConfig = {
|
||||
panels?: Array<PanelDef>;
|
||||
version?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The panels, and the address a device should be pointed at.
|
||||
*
|
||||
* The address is the server's own, because the browser's origin is not a
|
||||
* reliable answer to it: an admin working through the portal is on the
|
||||
* portal's origin, and this one is for a screen on this network.
|
||||
*
|
||||
* An instance enrolled with a portal has a second address, built by the
|
||||
* dialog from what ``/cloud/status`` reports rather than from here — a panel
|
||||
* is not the thing that knows whether remote access is on.
|
||||
*/
|
||||
export type PanelsPublic = {
|
||||
panels?: Array<PanelDef>;
|
||||
version?: number;
|
||||
frontend_host?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Who is asking, as far as the request itself says.
|
||||
*/
|
||||
export type PendingDevice = {
|
||||
request?: string;
|
||||
device: string;
|
||||
name?: string;
|
||||
board?: string;
|
||||
firmware?: string;
|
||||
remote?: boolean;
|
||||
needs_code?: boolean;
|
||||
expires_in?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1266,14 +1292,14 @@ export type RunRequest = {
|
||||
* called, which is all the matching needs.
|
||||
*/
|
||||
export type SearchEntry = {
|
||||
category: 'flow' | 'node' | 'input' | 'output' | 'dashboard' | 'widget' | 'panel' | 'secret' | 'module' | 'worker' | 'alert';
|
||||
category: 'flow' | 'node' | 'input' | 'output' | 'dashboard' | 'widget' | 'device' | 'secret' | 'module' | 'worker' | 'alert';
|
||||
name: string;
|
||||
title?: string;
|
||||
parent?: string;
|
||||
kind?: string;
|
||||
};
|
||||
|
||||
export type category = 'flow' | 'node' | 'input' | 'output' | 'dashboard' | 'widget' | 'panel' | 'secret' | 'module' | 'worker' | 'alert';
|
||||
export type category = 'flow' | 'node' | 'input' | 'output' | 'dashboard' | 'widget' | 'device' | 'secret' | 'module' | 'worker' | 'alert';
|
||||
|
||||
export type SecretNames = {
|
||||
data: Array<(string)>;
|
||||
@@ -1778,6 +1804,68 @@ export type DashboardsRenameDashboardData = {
|
||||
|
||||
export type DashboardsRenameDashboardResponse = (DashboardDef_Output);
|
||||
|
||||
export type DevicesStartPairingData = {
|
||||
requestBody?: (PairHello | null);
|
||||
};
|
||||
|
||||
export type DevicesStartPairingResponse = (PairStarted);
|
||||
|
||||
export type DevicesPollPairingData = {
|
||||
code: string;
|
||||
secret?: string;
|
||||
};
|
||||
|
||||
export type DevicesPollPairingResponse = (PairStatus);
|
||||
|
||||
export type DevicesReadDevicesResponse = (DevicesPublic);
|
||||
|
||||
export type DevicesReadPendingResponse = (Array<PendingDevice>);
|
||||
|
||||
export type DevicesPendingDeviceData = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type DevicesPendingDeviceResponse = (PendingDevice);
|
||||
|
||||
export type DevicesCreateDeviceData = {
|
||||
deviceId: string;
|
||||
requestBody?: (DeviceCreate | null);
|
||||
};
|
||||
|
||||
export type DevicesCreateDeviceResponse = (DeviceDef);
|
||||
|
||||
export type DevicesReadDeviceData = {
|
||||
deviceId: string;
|
||||
};
|
||||
|
||||
export type DevicesReadDeviceResponse = (DeviceDef);
|
||||
|
||||
export type DevicesSaveDeviceData = {
|
||||
deviceId: string;
|
||||
requestBody: DeviceDef;
|
||||
};
|
||||
|
||||
export type DevicesSaveDeviceResponse = (DeviceDef);
|
||||
|
||||
export type DevicesDeleteDeviceData = {
|
||||
deviceId: string;
|
||||
};
|
||||
|
||||
export type DevicesDeleteDeviceResponse = (Message);
|
||||
|
||||
export type DevicesApprovePairingData = {
|
||||
deviceId: string;
|
||||
requestBody: PairRequest;
|
||||
};
|
||||
|
||||
export type DevicesApprovePairingResponse = (Message);
|
||||
|
||||
export type DevicesUnpairDeviceData = {
|
||||
deviceId: string;
|
||||
};
|
||||
|
||||
export type DevicesUnpairDeviceResponse = (Message);
|
||||
|
||||
export type FlavorsReadFlavorsResponse = (FlavorsPublic);
|
||||
|
||||
export type FlavorsCreateFlavorData = {
|
||||
@@ -2108,48 +2196,6 @@ export type ObservabilityReadDeadLettersResponse = (Array<DeadLetter>);
|
||||
|
||||
export type ObservabilityFlushMetricsResponse = (Message);
|
||||
|
||||
export type PanelsReadPanelsResponse = (PanelsPublic);
|
||||
|
||||
export type PanelsSavePanelsData = {
|
||||
requestBody: PanelsConfig;
|
||||
};
|
||||
|
||||
export type PanelsSavePanelsResponse = (PanelsPublic);
|
||||
|
||||
export type PanelsStartPairingResponse = (PairStarted);
|
||||
|
||||
export type PanelsPollPairingData = {
|
||||
code: string;
|
||||
secret?: string;
|
||||
};
|
||||
|
||||
export type PanelsPollPairingResponse = (PairStatus);
|
||||
|
||||
export type PanelsPendingDeviceData = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
export type PanelsPendingDeviceResponse = (PendingDevice);
|
||||
|
||||
export type PanelsApprovePairingData = {
|
||||
panelId: string;
|
||||
requestBody: PairRequest;
|
||||
};
|
||||
|
||||
export type PanelsApprovePairingResponse = (Message);
|
||||
|
||||
export type PanelsUnpairPanelData = {
|
||||
panelId: string;
|
||||
};
|
||||
|
||||
export type PanelsUnpairPanelResponse = (Message);
|
||||
|
||||
export type PanelsReadPanelData = {
|
||||
panelId: string;
|
||||
};
|
||||
|
||||
export type PanelsReadPanelResponse = (PanelDef);
|
||||
|
||||
export type PrivateCreateUserData = {
|
||||
requestBody: PrivateUserCreate;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user