-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial mqtt calls for endpoints #403
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
85734b1
feat(ms2/engines): initial mqtt endpoints
FelipeTrost b6b4b13
feat(ms2/engines): endpoints for engines
FelipeTrost ff6c609
feat(ms2/engines): mqtt requests
FelipeTrost 1099bb6
feat(ms2/admin): engine view
FelipeTrost eeef5cc
refactor(ms2/mqtt)
FelipeTrost 0c815e3
fix(ms2/engine-endpoints): import
FelipeTrost 28f6d10
fix(ms2/admin-engines): make page dynamic
FelipeTrost 201ff18
feat(ms2/admin): added link to engines in sidebar
FelipeTrost e2f0b01
chore: format
FelipeTrost c9eeefd
Merge branch 'main' into engine/mqtt
FelipeTrost 9ee7945
fix(ms2/env-vars): mqtt env vars shouldn't be required yet
FelipeTrost 1797cf4
Merge branch 'engine/mqtt' of github.com:PROCEED-Labs/proceed into en…
FelipeTrost 60fa7e3
fix: possible undefined
FelipeTrost f5ce66c
fix: engine name key
FelipeTrost ba762b9
fix(ms2/build): unset env-var
FelipeTrost 107e306
Merge remote-tracking branch 'origin/main' into engine/mqtt
FelipeTrost 380fe3c
fix(ms2/mqtt): correct engine id key
FelipeTrost 4dca759
Merge remote-tracking branch 'origin/main' into engine/mqtt
FelipeTrost f9c841f
admin-dashboard/engines: better error message
FelipeTrost 83150ef
fix: import
FelipeTrost 033aa79
Merge branch 'main' into engine/mqtt
FelipeTrost File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
57 changes: 57 additions & 0 deletions
57
src/management-system-v2/app/admin/engines/engines-table.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
'use client'; | ||
|
||
import { Tag } from 'antd'; | ||
import { useState } from 'react'; | ||
import { type TableEngine } from './page'; | ||
import ElementList from '@/components/item-list-view'; | ||
import Bar from '@/components/bar'; | ||
import useFuzySearch from '@/lib/useFuzySearch'; | ||
|
||
export default function EnginesTable({ engines }: { engines: TableEngine[] }) { | ||
const { filteredData, searchQuery, setSearchQuery } = useFuzySearch({ | ||
data: engines, | ||
keys: ['name'], | ||
highlightedKeys: ['name'], | ||
transformData: (matches) => matches.map((match) => match.item), | ||
}); | ||
|
||
const [selectedEngines, setSelectedEngines] = useState<typeof filteredData>([]); | ||
|
||
return ( | ||
<> | ||
<Bar | ||
searchProps={{ | ||
value: searchQuery, | ||
onChange: (e) => setSearchQuery(e.target.value), | ||
onPressEnter: (e) => setSearchQuery(e.currentTarget.value), | ||
placeholder: 'Search spaces ...', | ||
}} | ||
/> | ||
|
||
<ElementList | ||
data={filteredData} | ||
elementSelection={{ | ||
selectedElements: selectedEngines, | ||
setSelectionElements: setSelectedEngines, | ||
}} | ||
columns={[ | ||
{ | ||
title: 'Engine ID', | ||
dataIndex: 'name', | ||
render: (_, engine) => engine.name.highlighted, | ||
}, | ||
{ | ||
title: 'Status', | ||
dataIndex: 'owner', | ||
sorter: (a, b) => +a.running - +b.running, | ||
render: (_, engine) => ( | ||
<Tag color={engine.running ? 'success' : 'error'}> | ||
{engine.running ? 'Online' : 'Offline'} | ||
</Tag> | ||
), | ||
}, | ||
]} | ||
/> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { getCurrentUser } from '@/components/auth'; | ||
import Content from '@/components/content'; | ||
import { getEngines } from '@/lib/engines/mqtt-endpoints'; | ||
import { Result, Skeleton } from 'antd'; | ||
import { notFound, redirect } from 'next/navigation'; | ||
import { Suspense } from 'react'; | ||
import { getSystemAdminByUserId } from '@/lib/data/DTOs'; | ||
import EnginesTable from './engines-table'; | ||
import { env } from '@/lib/env-vars'; | ||
|
||
export type TableEngine = Awaited<ReturnType<typeof getEngines>>[number] & { name: string }; | ||
|
||
async function Engines() { | ||
const user = await getCurrentUser(); | ||
if (!user.session) redirect('/'); | ||
const adminData = getSystemAdminByUserId(user.userId); | ||
if (!adminData) redirect('/'); | ||
|
||
try { | ||
const engines = (await getEngines()).map((e) => ({ ...e, name: e.id })); | ||
|
||
return <EnginesTable engines={engines} />; | ||
} catch (e) { | ||
console.error(e); | ||
return <Result status="500" title="Error" subTitle="Couldn't fetch engines" />; | ||
} | ||
} | ||
|
||
export default function EnginesPage() { | ||
if (!env.NEXT_PUBLIC_ENABLE_EXECUTION) return notFound(); | ||
|
||
if (!env.MQTT_SERVER_ADDRESS) | ||
return <Result status="500" title="Error" subTitle="No MQTT server address configured" />; | ||
|
||
return ( | ||
<Content title="Engines"> | ||
<Suspense fallback={<Skeleton active />}> | ||
<Engines /> | ||
</Suspense> | ||
</Content> | ||
); | ||
} | ||
|
||
export const dynamic = 'force-dynamic'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
type EndpointSchema = typeof import('./endpoints.json'); | ||
type Endpoints = EndpointSchema; | ||
type Methods = 'get' | 'post' | 'put' | 'delete'; | ||
|
||
type GetParamsFromString< | ||
Str extends string, | ||
Count extends unknown[] = [], | ||
> = Str extends `${infer Start}:${string}/${infer Rest}` | ||
? Str extends `${Start}:${infer Param}/${Rest}` | ||
? GetParamsFromString<Rest, [...Count, Param]> | ||
: Count | ||
: Str extends `${string}:${infer End}` | ||
? [...Count, End] | ||
: Count; | ||
|
||
type EndpointArgsArray<ParamsArray extends string[]> = ParamsArray extends [] | ||
? [] | ||
: [Record<ParamsArray[number], string>]; | ||
type EndpointArgs<Endpoint extends string> = EndpointArgsArray<GetParamsFromString<Endpoint>>; | ||
|
||
type AvailableEndpoints<Method extends Methods> = keyof Endpoints[Method] extends string | ||
? keyof Endpoints[Method] | ||
: never; | ||
export function endpointBuilder<Method extends Methods, Url extends AvailableEndpoints<Method>>( | ||
_: Method, | ||
endpoint: Url, | ||
...options: EndpointArgs<Url> | ||
) { | ||
return endpoint.replace(/:([^/]+)/g, (_, capture_group) => options[0]?.[capture_group] || ''); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
{ | ||
"get": { | ||
"/machine/:properties": { | ||
"params": true | ||
}, | ||
"/machine/": {}, | ||
"/capabilities/": {}, | ||
"/configuration/": {}, | ||
"/configuration/:key": {}, | ||
"/logging": {}, | ||
"/logging/status": {}, | ||
"/logging/standard": {}, | ||
"/logging/process": {}, | ||
"/logging/process/:definitionId": {}, | ||
"/logging/process/:definitionId/instance/:instanceId": {}, | ||
"/monitoring/": {}, | ||
"/tasklist/api/": {}, | ||
"/tasklist/api/userTask": {}, | ||
"/configuration/api/config": {}, | ||
"/logging/api/log": {}, | ||
"/": {}, | ||
"/process/": {}, | ||
"/process/:definitionId": {}, | ||
"/process/:definitionId/versions": {}, | ||
"/process/:definitionId/versions/:version": {}, | ||
"/process/:definitionId/instance": {}, | ||
"/process/:definitionId/instance/:instanceID": {}, | ||
"/process/:definitionId/user-tasks/:fileName": {}, | ||
"/process/:definitionId/user-tasks": {}, | ||
"/status/": {}, | ||
"/resources/process/:definitionId/images/:fileName": {}, | ||
"/resources/process/:definitionId/images/": {} | ||
}, | ||
"post": { | ||
"/capabilities/execute": {}, | ||
"/capabilities/return": {}, | ||
"/evaluation/": {}, | ||
"/tasklist/api/userTask": {}, | ||
"/configuration/api/config": {}, | ||
"/process/": {}, | ||
"/process/:definitionId/versions/:version/instance": {}, | ||
"/process/:definitionId/instance/:instanceId/tokens": {}, | ||
"/process/:definitionId/instance/:instanceId/variables": {}, | ||
"/process/:definitionId/versions/:version/instance/migration": {} | ||
}, | ||
"put": { | ||
"/configuration/": {}, | ||
"/tasklist/api/variable": {}, | ||
"/tasklist/api/milestone": {}, | ||
"/process/:definitionId/instance/:instanceID": {}, | ||
"/process/:definitionId/instance/:instanceID/instanceState": {}, | ||
"/process/:definitionId/instance/:instanceId/tokens/:tokenId": {}, | ||
"/process/:definitionId/instance/:instanceId/tokens/:tokenId/currentFlowNodeState": {}, | ||
"/process/:definitionId/user-tasks/:fileName": {}, | ||
"/resources/process/:definitionId/images/:fileName": {} | ||
}, | ||
"delete": { | ||
"/configuration/": {}, | ||
"/logging": {}, | ||
"/logging/standard": {}, | ||
"/logging/process": {}, | ||
"/logging/process/:definitionId": {}, | ||
"/logging/process/:definitionId/instance/:instanceId": {}, | ||
"/process/:definitionId": {}, | ||
"/process/:definitionId/instance/:instanceId/tokens/:tokenId": {} | ||
} | ||
} |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
import mqtt from 'mqtt'; | ||
import { env } from '@/lib/env-vars'; | ||
|
||
const mqttTimeout = 1000; | ||
|
||
const mqttCredentials = { | ||
password: env.MQTT_PASSWORD, | ||
username: env.MQTT_USERNAME, | ||
}; | ||
|
||
const baseTopicPrefix = env.MQTT_BASETOPIC ? env.MQTT_BASETOPIC + '/' : ''; | ||
|
||
export function getClient(options?: mqtt.IClientOptions): Promise<mqtt.MqttClient> { | ||
const address = env.MQTT_SERVER_ADDRESS || ''; | ||
|
||
return new Promise((res, rej) => { | ||
const client = mqtt.connect(address, { | ||
...mqttCredentials, | ||
...options, | ||
}); | ||
client.on('connect', () => res(client)); | ||
client.on('error', (err) => rej(err)); | ||
}); | ||
} | ||
|
||
function subscribeToTopic(client: mqtt.MqttClient, topic: string) { | ||
return new Promise<void>((res, rej) => { | ||
setTimeout(rej, mqttTimeout); // Timeout if the subscription takes too long | ||
client.subscribe(topic, (err) => { | ||
if (err) rej(err); | ||
res(); | ||
}); | ||
}); | ||
} | ||
|
||
function getEnginePrefix(engineId: string) { | ||
return `${baseTopicPrefix}proceed-pms/engine/${engineId}`; | ||
} | ||
|
||
export async function getEngines() { | ||
const client = await getClient({ | ||
connectTimeout: mqttTimeout, | ||
}); | ||
|
||
const engines: { id: string; running: boolean; version: string }[] = []; | ||
|
||
await subscribeToTopic(client, `${getEnginePrefix('+')}/status`); | ||
|
||
// All retained messages are sent at once | ||
// The broker should bundle them in one tcp packet, | ||
// after it is parsed all messages are in the queue, and handled before close | ||
// is handled, as the packets where pushed to the queue before the close event was emitted. | ||
// This is of course subject to the implementation of the broker, | ||
// however for a small amount of engines it should be fine. | ||
await new Promise<void>((res) => { | ||
setTimeout(res, mqttTimeout); // Timeout in case we receive no messages | ||
|
||
client.on('message', (topic, message) => { | ||
const match = topic.match(new RegExp(`^${getEnginePrefix('')}([^\/]+)\/status`)); | ||
if (match) { | ||
const id = match[1]; | ||
const status = JSON.parse(message.toString()); | ||
engines.push({ id, ...status }); | ||
res(); | ||
} | ||
}); | ||
}); | ||
|
||
await client.endAsync(); | ||
|
||
return engines; | ||
} | ||
|
||
const requestClient = getClient(); | ||
|
||
export async function mqttRequest( | ||
engineId: string, | ||
url: string, | ||
message: { | ||
method: 'GET' | 'POST' | 'PUT' | 'DELETE'; | ||
body: Record<string, any>; | ||
query: Record<string, any>; | ||
page?: number; | ||
}, | ||
) { | ||
const client = await requestClient; | ||
|
||
const requestId = crypto.randomUUID(); | ||
const requestTopic = getEnginePrefix(engineId) + '/api' + url; | ||
await subscribeToTopic(client, requestTopic); | ||
|
||
// handler for the response | ||
let res: (res: any) => void, rej: (Err: any) => void; | ||
const receivedAnswer = new Promise<any>((_res, _rej) => { | ||
res = _res; | ||
rej = _rej; | ||
}); | ||
function handler(topic: string, _message: any) { | ||
const message = JSON.parse(_message.toString()); | ||
if (topic !== requestTopic) return; | ||
if ( | ||
!message || | ||
typeof message !== 'object' || | ||
!('type' in message) || | ||
message.type !== 'response' || | ||
!('id' in message) || | ||
message.id !== requestId | ||
) | ||
return; | ||
|
||
res(JSON.parse(message.body)); | ||
} | ||
client.on('message', handler); | ||
|
||
// send request | ||
client.publish(requestTopic, JSON.stringify({ ...message, type: 'request', id: requestId })); | ||
|
||
// await for response or timeout | ||
setTimeout(rej!, mqttTimeout); | ||
const response = await receivedAnswer; | ||
|
||
// cleanup | ||
client.removeListener('message', handler); | ||
|
||
return response; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would maybe add one or two comments to this file to explain what exactly these types do.
And is "Count" actually a fitting name? I looks like it is a list of all arguments that are behind a ":" in the string.