diff --git a/api/proto/api/application.proto b/api/proto/api/application.proto index d009c700b..05439f4f7 100644 --- a/api/proto/api/application.proto +++ b/api/proto/api/application.proto @@ -96,6 +96,40 @@ service ApplicationService { }; } + // Create Qubitro integration. + rpc CreateQubitroIntegration(CreateQubitroIntegrationRequest) + returns (google.protobuf.Empty) { + option (google.api.http) = { + post : "/api/applications/{integration.application_id}/integrations/qubitro" + body : "*" + }; + } + + // Get Qubitro integration. + rpc GetQubitroIntegration(GetQubitroIntegrationRequest) + returns (GetQubitroIntegrationResponse) { + option (google.api.http) = { + get : "/api/applications/{application_id}/integrations/qubitro" + }; + } + + // Update Qubitro integration. + rpc UpdateQubitroIntegration(UpdateQubitroIntegrationRequest) + returns (google.protobuf.Empty) { + option (google.api.http) = { + put : "/api/applications/{integration.application_id}/integrations/qubitro" + body : "*" + }; + } + + // Delete Qubitro integration. + rpc DeleteQubitroIntegration(DeleteQubitroIntegrationRequest) + returns (google.protobuf.Empty) { + option (google.api.http) = { + delete : "/api/applications/{application_id}/integrations/qubitro" + }; + } + // Create InfluxDb integration. rpc CreateInfluxDbIntegration(CreateInfluxDbIntegrationRequest) returns (google.protobuf.Empty) { @@ -460,6 +494,7 @@ enum IntegrationKind { PILOT_THINGS = 8; MQTT_GLOBAL = 9; IFTTT = 10; + QUBITRO = 11; } message Application { @@ -1149,3 +1184,39 @@ message ListApplicationDeviceTagsResponse { // Device tags. repeated ApplicationDeviceTagListItem result = 1; } + +message QubitroIntegration { + // Application ID (UUID). + string application_id = 1; + + // Qubitro Project ID. + string project_id = 2; + + // Qubitro Webhook Signing Key. + string webhook_signing_key = 3; +} + +message CreateQubitroIntegrationRequest { + // Integration object to create. + QubitroIntegration integration = 1; +} + +message GetQubitroIntegrationRequest { + // Application ID (UUID). + string application_id = 1; +} + +message GetQubitroIntegrationResponse { + // Integration object. + QubitroIntegration integration = 1; +} + +message UpdateQubitroIntegrationRequest { + // Integration object to update. + QubitroIntegration integration = 1; +} + +message DeleteQubitroIntegrationRequest { + // Application ID (UUID). + string application_id = 1; +} diff --git a/api/rust/proto/chirpstack/api/application.proto b/api/rust/proto/chirpstack/api/application.proto index d009c700b..3bc9c49f0 100644 --- a/api/rust/proto/chirpstack/api/application.proto +++ b/api/rust/proto/chirpstack/api/application.proto @@ -96,6 +96,40 @@ service ApplicationService { }; } + // Create Qubitro integration. + rpc CreateQubitroIntegration(CreateQubitroIntegrationRequest) + returns (google.protobuf.Empty) { + option (google.api.http) = { + post : "/api/applications/{integration.application_id}/integrations/qubitro" + body : "*" + }; + } + + // Get Qubitro integration. + rpc GetQubitroIntegration(GetQubitroIntegrationRequest) + returns (GetQubitroIntegrationResponse) { + option (google.api.http) = { + get : "/api/applications/{application_id}/integrations/qubitro" + }; + } + + // Update Qubitro integration. + rpc UpdateQubitroIntegration(UpdateQubitroIntegrationRequest) + returns (google.protobuf.Empty) { + option (google.api.http) = { + put : "/api/applications/{integration.application_id}/integrations/qubitro" + body : "*" + }; + } + + // Delete Qubitro integration. + rpc DeleteQubitroIntegration(DeleteQubitroIntegrationRequest) + returns (google.protobuf.Empty) { + option (google.api.http) = { + delete : "/api/applications/{application_id}/integrations/qubitro" + }; + } + // Create InfluxDb integration. rpc CreateInfluxDbIntegration(CreateInfluxDbIntegrationRequest) returns (google.protobuf.Empty) { @@ -1149,3 +1183,39 @@ message ListApplicationDeviceTagsResponse { // Device tags. repeated ApplicationDeviceTagListItem result = 1; } + +message QubitroIntegration { + // Application ID (UUID). + string application_id = 1; + + // Qubitro Project ID. + string project_id = 2; + + // Qubitro Webhook Signing Key. + string webhook_signing_key = 3; +} + +message CreateQubitroIntegrationRequest { + // Integration object to create. + QubitroIntegration integration = 1; +} + +message GetQubitroIntegrationRequest { + // Application ID (UUID). + string application_id = 1; +} + +message GetQubitroIntegrationResponse { + // Integration object. + QubitroIntegration integration = 1; +} + +message UpdateQubitroIntegrationRequest { + // Integration object to update. + QubitroIntegration integration = 1; +} + +message DeleteQubitroIntegrationRequest { + // Application ID (UUID). + string application_id = 1; +} diff --git a/chirpstack/src/api/application.rs b/chirpstack/src/api/application.rs index 028113a93..ffdd8f78f 100644 --- a/chirpstack/src/api/application.rs +++ b/chirpstack/src/api/application.rs @@ -1962,6 +1962,130 @@ impl ApplicationService for Application { Ok(resp) } + + async fn create_qubitro_integration( + &self, + request: Request, + ) -> Result, Status> { + let req_int = match &request.get_ref().integration { + Some(v) => v, + None => { + return Err(Status::invalid_argument("integration is missing")); + } + }; + let app_id = Uuid::from_str(&req_int.application_id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateApplicationAccess::new(validator::Flag::Update, app_id), + ) + .await?; + + let _ = application::create_qubitro_integration(application::QubitroConfiguration { + application_id: app_id.into(), + project_id: req_int.project_id.clone(), + webhook_signing_key: req_int.webhook_signing_key.clone(), + }) + .await + .map_err(|e| e.status())?; + + let mut resp = Response::new(()); + resp.metadata_mut() + .insert("x-log-application_id", req_int.application_id.parse().unwrap()); + + Ok(resp) + } + + async fn get_qubitro_integration( + &self, + request: Request, + ) -> Result, Status> { + let req = request.get_ref(); + let app_id = Uuid::from_str(&req.application_id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateApplicationAccess::new(validator::Flag::Read, app_id), + ) + .await?; + + let i = application::get_qubitro_integration(&app_id) + .await + .map_err(|e| e.status())?; + + let mut resp = Response::new(api::GetQubitroIntegrationResponse { + integration: Some(api::QubitroIntegration { + application_id: i.application_id.to_string(), + project_id: i.project_id, + webhook_signing_key: i.webhook_signing_key, + }), + }); + resp.metadata_mut() + .insert("x-log-application_id", req.application_id.parse().unwrap()); + + Ok(resp) + } + + async fn update_qubitro_integration( + &self, + request: Request, + ) -> Result, Status> { + let req_int = match &request.get_ref().integration { + Some(v) => v, + None => { + return Err(Status::invalid_argument("integration is missing")); + } + }; + let app_id = Uuid::from_str(&req_int.application_id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateApplicationAccess::new(validator::Flag::Update, app_id), + ) + .await?; + + let _ = application::update_qubitro_integration(application::QubitroConfiguration { + application_id: app_id.into(), + project_id: req_int.project_id.clone(), + webhook_signing_key: req_int.webhook_signing_key.clone(), + }) + .await + .map_err(|e| e.status())?; + + let mut resp = Response::new(()); + resp.metadata_mut() + .insert("x-log-application_id", req_int.application_id.parse().unwrap()); + + Ok(resp) + } + + async fn delete_qubitro_integration( + &self, + request: Request, + ) -> Result, Status> { + let req = request.get_ref(); + let app_id = Uuid::from_str(&req.application_id).map_err(|e| e.status())?; + + self.validator + .validate( + request.extensions(), + validator::ValidateApplicationAccess::new(validator::Flag::Update, app_id), + ) + .await?; + + application::delete_qubitro_integration(&app_id) + .await + .map_err(|e| e.status())?; + + let mut resp = Response::new(()); + resp.metadata_mut() + .insert("x-log-application_id", req.application_id.parse().unwrap()); + + Ok(resp) + } } #[cfg(test)] @@ -3561,4 +3685,135 @@ pub mod test { list_resp ); } + + #[tokio::test] + async fn test_qubitro_integration() { + let _guard = test::prepare().await; + let app = get_application().await; + let u = get_user().await; + let service = Application::new(RequestValidator::new()); + + // create + let create_req = get_request( + &u.id, + api::CreateQubitroIntegrationRequest { + integration: Some(api::QubitroIntegration { + application_id: app.id.to_string(), + project_id: "test-project".into(), + webhook_signing_key: "test-key".into(), + }), + }, + ); + let _ = service + .create_qubitro_integration(create_req) + .await + .unwrap(); + + // get + let get_req = get_request( + &u.id, + api::GetQubitroIntegrationRequest { + application_id: app.id.to_string(), + }, + ); + let get_resp = service.get_qubitro_integration(get_req).await.unwrap(); + let get_resp = get_resp.get_ref(); + assert_eq!( + Some(api::QubitroIntegration { + application_id: app.id.to_string(), + project_id: "test-project".into(), + webhook_signing_key: "test-key".into(), + }), + get_resp.integration + ); + + // update + let update_req = get_request( + &u.id, + api::UpdateQubitroIntegrationRequest { + integration: Some(api::QubitroIntegration { + application_id: app.id.to_string(), + project_id: "test-project-updated".into(), + webhook_signing_key: "test-key-updated".into(), + }), + }, + ); + let _ = service + .update_qubitro_integration(update_req) + .await + .unwrap(); + + // get + let get_req = get_request( + &u.id, + api::GetQubitroIntegrationRequest { + application_id: app.id.to_string(), + }, + ); + let get_resp = service.get_qubitro_integration(get_req).await.unwrap(); + let get_resp = get_resp.get_ref(); + assert_eq!( + Some(api::QubitroIntegration { + application_id: app.id.to_string(), + project_id: "test-project-updated".into(), + webhook_signing_key: "test-key-updated".into(), + }), + get_resp.integration + ); + + // list + let list_req = get_request( + &u.id, + api::ListIntegrationsRequest { + application_id: app.id.to_string(), + }, + ); + let list_resp = service.list_integrations(list_req).await.unwrap(); + let list_resp = list_resp.get_ref(); + assert_eq!( + &api::ListIntegrationsResponse { + total_count: 2, + result: vec![ + api::IntegrationListItem { + kind: api::IntegrationKind::Qubitro.into(), + }, + api::IntegrationListItem { + kind: api::IntegrationKind::MqttGlobal.into(), + } + ], + }, + list_resp + ); + + // delete + let del_req = get_request( + &u.id, + api::DeleteQubitroIntegrationRequest { + application_id: app.id.to_string(), + }, + ); + let _ = service + .delete_qubitro_integration(del_req) + .await + .unwrap(); + + // list + let list_req = get_request( + &u.id, + api::ListIntegrationsRequest { + application_id: app.id.to_string(), + }, + ); + let list_resp = service.list_integrations(list_req).await.unwrap(); + let list_resp = list_resp.get_ref(); + assert_eq!( + &api::ListIntegrationsResponse { + total_count: 1, + result: vec![api::IntegrationListItem { + kind: api::IntegrationKind::MqttGlobal.into(), + },], + }, + list_resp + ); + } } diff --git a/chirpstack/src/integration/qubitro.rs b/chirpstack/src/integration/qubitro.rs new file mode 100644 index 000000000..82e20c443 --- /dev/null +++ b/chirpstack/src/integration/qubitro.rs @@ -0,0 +1,187 @@ +use std::collections::HashMap; +use std::sync::OnceLock; +use std::time::Duration; + +use anyhow::Result; +use async_trait::async_trait; +use reqwest::header::{HeaderMap, HeaderName, CONTENT_TYPE}; +use reqwest::Client; +use tracing::{info, trace, warn}; + +use super::Integration as IntegrationTrait; +use crate::storage::application::QubitroConfiguration; +use chirpstack_api::integration; + +static CLIENT: OnceLock = OnceLock::new(); +static QUBITRO_ENDPOINT: &str = "https://webhook.qubitro.com/integrations/chirpstack"; + +fn get_client() -> Client { + CLIENT + .get_or_init(|| { + Client::builder() + .timeout(Duration::from_secs(5)) + .build() + .unwrap() + }) + .clone() +} + +pub struct Integration { + project_id: String, + webhook_signing_key: String, +} + +impl Integration { + pub fn new(conf: &QubitroConfiguration) -> Integration { + trace!("Initializing Qubitro integration"); + + Integration { + project_id: conf.project_id.clone(), + webhook_signing_key: conf.webhook_signing_key.clone(), + } + } + + async fn post_event(&self, event: &str, b: Vec) -> Result<()> { + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, "application/json".parse().unwrap()); + headers.insert( + HeaderName::from_static("projectId"), + self.project_id.parse()?, + ); + headers.insert( + HeaderName::from_static("webhookSigningKey"), + self.webhook_signing_key.parse()?, + ); + + info!(event = %event, "Posting event to Qubitro"); + let res = get_client() + .post(QUBITRO_ENDPOINT) + .body(b) + .query(&[("event", event)]) + .headers(headers) + .send() + .await; + + match res { + Ok(res) => match res.error_for_status() { + Ok(_) => {} + Err(e) => { + warn!(event = %event, error = %e, "Posting event to Qubitro failed"); + } + }, + Err(e) => { + warn!(event = %event, error = %e, "Posting event to Qubitro failed"); + } + } + + Ok(()) + } +} + +#[async_trait] +impl IntegrationTrait for Integration { + async fn uplink_event( + &self, + _vars: &HashMap, + pl: &integration::UplinkEvent, + ) -> Result<()> { + let b = serde_json::to_vec(&pl)?; + self.post_event("up", b).await + } + + async fn join_event( + &self, + _vars: &HashMap, + pl: &integration::JoinEvent, + ) -> Result<()> { + let b = serde_json::to_vec(&pl)?; + self.post_event("join", b).await + } + + async fn ack_event( + &self, + _vars: &HashMap, + pl: &integration::AckEvent, + ) -> Result<()> { + let b = serde_json::to_vec(&pl)?; + self.post_event("ack", b).await + } + + async fn txack_event( + &self, + _vars: &HashMap, + pl: &integration::TxAckEvent, + ) -> Result<()> { + let b = serde_json::to_vec(&pl)?; + self.post_event("txack", b).await + } + + async fn log_event( + &self, + _vars: &HashMap, + pl: &integration::LogEvent, + ) -> Result<()> { + let b = serde_json::to_vec(&pl)?; + self.post_event("log", b).await + } + + async fn status_event( + &self, + _vars: &HashMap, + pl: &integration::StatusEvent, + ) -> Result<()> { + let b = serde_json::to_vec(&pl)?; + self.post_event("status", b).await + } + + async fn location_event( + &self, + _vars: &HashMap, + pl: &integration::LocationEvent, + ) -> Result<()> { + let b = serde_json::to_vec(&pl)?; + self.post_event("location", b).await + } + + async fn integration_event( + &self, + _vars: &HashMap, + pl: &integration::IntegrationEvent, + ) -> Result<()> { + let b = serde_json::to_vec(&pl)?; + self.post_event("integration", b).await + } +} + +#[cfg(test)] +pub mod test { + use super::*; + use httpmock::prelude::*; + + #[tokio::test] + async fn test_qubitro() { + let server = MockServer::start(); + + let i = Integration { + project_id: "eeaff160-1628-490a-b380-fb374dfb6584".to_string(), + webhook_signing_key: "229f57f57504ceb9dbfd0d6af8b4ee2c8330ea2bb443cfeb0753d0086773541472ffcfc20affb9d7fc62778beee4159f3a67006fdca0a233d74595500a722e94".to_string(), + }; + + // uplink event + let pl: integration::UplinkEvent = Default::default(); + let mut mock = server.mock(|when, then| { + when.method(POST) + .path("/") + .header("projectId", "eeaff160-1628-490a-b380-fb374dfb6584") + .header("webhookSigningKey", "229f57f57504ceb9dbfd0d6af8b4ee2c8330ea2bb443cfeb0753d0086773541472ffcfc20affb9d7fc62778beee4159f3a67006fdca0a233d74595500a722e94") + .header("content-type", "application/json") + .body(serde_json::to_string(&pl).unwrap()); + + then.status(200); + }); + + i.uplink_event(&HashMap::new(), &pl).await.unwrap(); + mock.assert(); + mock.delete(); + } +} \ No newline at end of file diff --git a/chirpstack/src/storage/application.rs b/chirpstack/src/storage/application.rs index 063d987f4..750d75fdc 100644 --- a/chirpstack/src/storage/application.rs +++ b/chirpstack/src/storage/application.rs @@ -85,6 +85,7 @@ pub enum IntegrationKind { AzureServiceBus, PilotThings, Ifttt, + Qubitro, } impl fmt::Display for IntegrationKind { @@ -108,6 +109,7 @@ impl FromStr for IntegrationKind { "AzureServiceBus" => IntegrationKind::AzureServiceBus, "PilotThings" => IntegrationKind::PilotThings, "Ifttt" => IntegrationKind::Ifttt, + "Qubitro" => IntegrationKind::Qubitro, _ => { return Err(anyhow!("Unexpected IntegrationKind: {}", s)); } @@ -158,6 +160,7 @@ pub enum IntegrationConfiguration { AzureServiceBus(AzureServiceBusConfiguration), PilotThings(PilotThingsConfiguration), Ifttt(IftttConfiguration), + Qubitro(QubitroConfiguration), } #[cfg(feature = "postgres")] @@ -290,6 +293,12 @@ pub struct IftttConfiguration { pub event_prefix: String, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct QubitroConfiguration { + pub project_id: String, + pub webhook_signing_key: String, +} + #[derive(Clone, Queryable, Insertable, PartialEq, Eq, Debug)] #[diesel(table_name = application_integration)] pub struct Integration { diff --git a/ui/public/integrations/qubitro.png b/ui/public/integrations/qubitro.png new file mode 100644 index 000000000..19721435b Binary files /dev/null and b/ui/public/integrations/qubitro.png differ diff --git a/ui/src/stores/ApplicationStore.ts b/ui/src/stores/ApplicationStore.ts index eab9f4340..718bc9740 100644 --- a/ui/src/stores/ApplicationStore.ts +++ b/ui/src/stores/ApplicationStore.ts @@ -68,6 +68,11 @@ import type { ListApplicationDeviceProfilesResponse, ListApplicationDeviceTagsRequest, ListApplicationDeviceTagsResponse, + CreateQubitroIntegrationRequest, + GetQubitroIntegrationRequest, + GetQubitroIntegrationResponse, + UpdateQubitroIntegrationRequest, + DeleteQubitroIntegrationRequest, } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb"; import SessionStore from "./SessionStore"; @@ -832,6 +837,66 @@ class ApplicationStore extends EventEmitter { callbackFunc(resp); }); }; + + createQubitroIntegration = (req: CreateQubitroIntegrationRequest, callbackFunc: () => void) => { + this.client.createQubitroIntegration(req, SessionStore.getMetadata(), err => { + if (err !== null) { + HandleError(err); + return; + } + + notification.success({ + message: "Qubitro integration created", + duration: 3, + }); + + callbackFunc(); + }); + }; + + getQubitroIntegration = (req: GetQubitroIntegrationRequest, callbackFunc: (resp: GetQubitroIntegrationResponse) => void) => { + this.client.getQubitroIntegration(req, SessionStore.getMetadata(), (err, resp) => { + if (err !== null) { + HandleError(err); + return; + } + + callbackFunc(resp); + }); + }; + + updateQubitroIntegration = (req: UpdateQubitroIntegrationRequest, callbackFunc: () => void) => { + this.client.updateQubitroIntegration(req, SessionStore.getMetadata(), err => { + if (err !== null) { + HandleError(err); + return; + } + + notification.success({ + message: "Qubitro integration updated", + duration: 3, + }); + + callbackFunc(); + }); + }; + + deleteQubitroIntegration = (req: DeleteQubitroIntegrationRequest, callbackFunc: () => void) => { + this.client.deleteQubitroIntegration(req, SessionStore.getMetadata(), err => { + if (err !== null) { + HandleError(err); + return; + } + + notification.success({ + message: "Qubitro integration deleted", + duration: 3, + }); + + this.emit("integration.delete"); + callbackFunc(); + }); + }; } const applicationStore = new ApplicationStore(); diff --git a/ui/src/views/applications/integrations/CreateQubitroIntegration.tsx b/ui/src/views/applications/integrations/CreateQubitroIntegration.tsx new file mode 100644 index 000000000..03cb7243b --- /dev/null +++ b/ui/src/views/applications/integrations/CreateQubitroIntegration.tsx @@ -0,0 +1,47 @@ +import { Card } from "antd"; +import { useNavigate, useParams } from "react-router-dom"; + +import QubitroIntegrationForm from "./QubitroIntegrationForm"; +import ApplicationStore from "../../../stores/ApplicationStore"; +import { QubitroIntegration, CreateQubitroIntegrationRequest } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb"; + +function CreateQubitroIntegration() { + const { applicationId } = useParams(); + const navigate = useNavigate(); + + const onFinish = async (obj: QubitroIntegration.AsObject) => { + try { + const integration = new QubitroIntegration(); + integration.setApplicationId(applicationId!); + integration.setProjectId(obj.projectId); + integration.setWebhookSigningKey(obj.webhookSigningKey); + + const request = new CreateQubitroIntegrationRequest(); + request.setIntegration(integration); + + await new Promise((resolve, reject) => { + ApplicationStore.createQubitroIntegration(request, () => { + resolve(); + }); + }); + + navigate(`/applications/${applicationId}/integrations`); + } catch (e) { + console.error(e); + } + }; + + const initialValues: QubitroIntegration.AsObject = { + applicationId: applicationId!, + projectId: "", + webhookSigningKey: "", + }; + + return ( + + + + ); +} + +export default CreateQubitroIntegration; \ No newline at end of file diff --git a/ui/src/views/applications/integrations/EditQubitroIntegration.tsx b/ui/src/views/applications/integrations/EditQubitroIntegration.tsx new file mode 100644 index 000000000..06356c25c --- /dev/null +++ b/ui/src/views/applications/integrations/EditQubitroIntegration.tsx @@ -0,0 +1,70 @@ +import { Card } from "antd"; +import { useNavigate, useParams } from "react-router-dom"; +import { useState, useEffect } from "react"; + +import QubitroIntegrationForm from "./QubitroIntegrationForm"; +import ApplicationStore from "../../../stores/ApplicationStore"; +import { + QubitroIntegration, + UpdateQubitroIntegrationRequest, + GetQubitroIntegrationRequest, + GetQubitroIntegrationResponse, +} from "@chirpstack/chirpstack-api-grpc-web/api/application_pb"; + +function EditQubitroIntegration() { + const { applicationId } = useParams(); + const navigate = useNavigate(); + const [integration, setIntegration] = useState(undefined); + + useEffect(() => { + const fetchIntegration = () => { + const req = new GetQubitroIntegrationRequest(); + req.setApplicationId(applicationId!); + + ApplicationStore.getQubitroIntegration(req, (resp: GetQubitroIntegrationResponse) => { + const integration = resp.getIntegration()!; + setIntegration({ + applicationId: integration.getApplicationId(), + projectId: integration.getProjectId(), + webhookSigningKey: integration.getWebhookSigningKey(), + }); + }); + }; + + fetchIntegration(); + }, [applicationId]); + + const onFinish = async (obj: QubitroIntegration.AsObject) => { + try { + const integration = new QubitroIntegration(); + integration.setApplicationId(applicationId!); + integration.setProjectId(obj.projectId); + integration.setWebhookSigningKey(obj.webhookSigningKey); + + const request = new UpdateQubitroIntegrationRequest(); + request.setIntegration(integration); + + await new Promise((resolve, reject) => { + ApplicationStore.updateQubitroIntegration(request, () => { + resolve(); + }); + }); + + navigate(`/applications/${applicationId}/integrations`); + } catch (e) { + console.error(e); + } + }; + + if (integration === undefined) { + return null; + } + + return ( + + + + ); +} + +export default EditQubitroIntegration; \ No newline at end of file diff --git a/ui/src/views/applications/integrations/QubitroCard.tsx b/ui/src/views/applications/integrations/QubitroCard.tsx new file mode 100644 index 000000000..4f748408a --- /dev/null +++ b/ui/src/views/applications/integrations/QubitroCard.tsx @@ -0,0 +1,50 @@ +import { Card, Space } from "antd"; +import { Link } from "react-router-dom"; + +import DeleteConfirm from "../../../components/DeleteConfirm"; +import ApplicationStore from "../../../stores/ApplicationStore"; +import { DeleteQubitroIntegrationRequest } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb"; + +interface IProps { + add?: boolean; + applicationId: string; +} + +function QubitroCard({ add = false, applicationId }: IProps) { + const deleteIntegration = () => { + const req = new DeleteQubitroIntegrationRequest(); + req.setApplicationId(applicationId); + + ApplicationStore.deleteQubitroIntegration(req, () => { + // Callback after successful deletion + }); + }; + + let actions: any[] = []; + + if (add) { + actions = [Add]; + } else { + actions = [ + Edit, + , + ]; + } + + return ( + } + actions={actions} + > + +

+ The Qubitro integration forwards events to your Qubitro project using the Qubitro HTTP API. +

+
+
+ ); +} + +export default QubitroCard; \ No newline at end of file diff --git a/ui/src/views/applications/integrations/QubitroIntegrationForm.tsx b/ui/src/views/applications/integrations/QubitroIntegrationForm.tsx new file mode 100644 index 000000000..b2c03963d --- /dev/null +++ b/ui/src/views/applications/integrations/QubitroIntegrationForm.tsx @@ -0,0 +1,36 @@ +import { Form, Input, Button } from "antd"; + +import { QubitroIntegration } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb"; + +interface FormProps { + initialValues: QubitroIntegration.AsObject; + onFinish: (values: QubitroIntegration.AsObject) => void; +} + +function QubitroIntegrationForm({ initialValues, onFinish }: FormProps) { + return ( +
+ + + + + + + + + +
+ ); +} + +export default QubitroIntegrationForm; \ No newline at end of file