Add tags to tenants and applications.

Note that the integration events will contain the application +
device-profile + device tags. Integration events will NOT contain the
tenant tags. Most likely tenant tags will be used to store information
about the tenant, data that is unrelated to the integration events.

Fixes #211.
This commit is contained in:
Orne Brocaar
2023-10-19 17:11:50 +01:00
parent c7e586a326
commit a087c4c18b
24 changed files with 1005 additions and 831 deletions

View File

@ -459,6 +459,12 @@ message Application {
// Tenant ID (UUID). // Tenant ID (UUID).
string tenant_id = 4; string tenant_id = 4;
// Tags (user defined).
// These tags can be used to add additional information to the application.
// These tags are exposed in all the integration events of devices under
// this application.
map<string, string> tags = 5;
} }
message ApplicationListItem { message ApplicationListItem {

View File

@ -208,8 +208,8 @@ message Device {
map<string, string> variables = 8; map<string, string> variables = 8;
// Tags (user defined). // Tags (user defined).
// These tags are exposed in the event payloads or to integration. Tags are // These tags can be used to add additional information to the device.
// intended for aggregation and filtering. // These tags are exposed in all the integration events.
map<string, string> tags = 9; map<string, string> tags = 9;
// JoinEUI (optional, EUI64). // JoinEUI (optional, EUI64).

View File

@ -28,7 +28,8 @@ enum MeasurementKind {
// Unknown (in which case it is not tracked). // Unknown (in which case it is not tracked).
UNKNOWN = 0; UNKNOWN = 0;
// Incrementing counters that never decrease (these are not reset on each reading). // Incrementing counters that never decrease (these are not reset on each
// reading).
COUNTER = 1; COUNTER = 1;
// Counters that do get reset upon reading. // Counters that do get reset upon reading.
@ -79,7 +80,6 @@ enum SecondChAckOffset {
// 3200 kHz. // 3200 kHz.
KHZ_3200 = 5; KHZ_3200 = 5;
} }
enum RelayModeActivation { enum RelayModeActivation {
@ -96,7 +96,8 @@ enum RelayModeActivation {
END_DEVICE_CONTROLLED = 3; END_DEVICE_CONTROLLED = 3;
} }
// DeviceProfileService is the service providing API methods for managing device-profiles. // DeviceProfileService is the service providing API methods for managing
// device-profiles.
service DeviceProfileService { service DeviceProfileService {
// Create the given device-profile. // Create the given device-profile.
rpc Create(CreateDeviceProfileRequest) returns (CreateDeviceProfileResponse) { rpc Create(CreateDeviceProfileRequest) returns (CreateDeviceProfileResponse) {
@ -136,7 +137,8 @@ service DeviceProfileService {
} }
// List available ADR algorithms. // List available ADR algorithms.
rpc ListAdrAlgorithms(google.protobuf.Empty) returns (ListDeviceProfileAdrAlgorithmsResponse) { rpc ListAdrAlgorithms(google.protobuf.Empty)
returns (ListDeviceProfileAdrAlgorithmsResponse) {
option (google.api.http) = { option (google.api.http) = {
get : "/api/device-profiles/adr-algorithms" get : "/api/device-profiles/adr-algorithms"
}; };
@ -185,8 +187,8 @@ message DeviceProfile {
uint32 uplink_interval = 11; uint32 uplink_interval = 11;
// Device-status request interval (times / day). // Device-status request interval (times / day).
// This defines the times per day that ChirpStack will request the device-status // This defines the times per day that ChirpStack will request the
// from the device. // device-status from the device.
uint32 device_status_req_interval = 12; uint32 device_status_req_interval = 12;
// Supports OTAA. // Supports OTAA.
@ -199,7 +201,8 @@ message DeviceProfile {
bool supports_class_c = 15; bool supports_class_c = 15;
// Class-B timeout (seconds). // Class-B timeout (seconds).
// This is the maximum time ChirpStack will wait to receive an acknowledgement from the device (if requested). // This is the maximum time ChirpStack will wait to receive an acknowledgement
// from the device (if requested).
uint32 class_b_timeout = 16; uint32 class_b_timeout = 16;
// Class-B ping-slots per beacon period. // Class-B ping-slots per beacon period.
@ -215,7 +218,8 @@ message DeviceProfile {
uint32 class_b_ping_slot_freq = 19; uint32 class_b_ping_slot_freq = 19;
// Class-C timeout (seconds). // Class-C timeout (seconds).
// This is the maximum time ChirpStack will wait to receive an acknowledgement from the device (if requested). // This is the maximum time ChirpStack will wait to receive an acknowledgement
// from the device (if requested).
uint32 class_c_timeout = 20; uint32 class_c_timeout = 20;
// RX1 delay (for ABP). // RX1 delay (for ABP).
@ -230,7 +234,10 @@ message DeviceProfile {
// RX2 frequency (for ABP, Hz). // RX2 frequency (for ABP, Hz).
uint32 abp_rx2_freq = 24; uint32 abp_rx2_freq = 24;
// User defined tags. // Tags (user defined).
// These tags can be used to add additional information the the
// device-profile. These tags are exposed in all the integration events of
// devices using this device-profile.
map<string, string> tags = 25; map<string, string> tags = 25;
// Measurements. // Measurements.

View File

@ -122,6 +122,11 @@ message Tenant {
// do want to share uplinks with other tenants (private_gateways_up=false), // do want to share uplinks with other tenants (private_gateways_up=false),
// but you want to prevent other tenants from using gateway airtime. // but you want to prevent other tenants from using gateway airtime.
bool private_gateways_down = 8; bool private_gateways_down = 8;
// Tags (user defined).
// These tags can be used to add additional information to the tenant. These
// tags are NOT exposed in the integration events.
map<string, string> tags = 9;
} }
message TenantListItem { message TenantListItem {

View File

@ -459,6 +459,12 @@ message Application {
// Tenant ID (UUID). // Tenant ID (UUID).
string tenant_id = 4; string tenant_id = 4;
// Tags (user defined).
// These tags can be used to add additional information to the application.
// These tags are exposed in all the integration events of devices under
// this application.
map<string, string> tags = 5;
} }
message ApplicationListItem { message ApplicationListItem {

View File

@ -208,8 +208,8 @@ message Device {
map<string, string> variables = 8; map<string, string> variables = 8;
// Tags (user defined). // Tags (user defined).
// These tags are exposed in the event payloads or to integration. Tags are // These tags can be used to add additional information to the device.
// intended for aggregation and filtering. // These tags are exposed in all the integration events.
map<string, string> tags = 9; map<string, string> tags = 9;
// JoinEUI (optional, EUI64). // JoinEUI (optional, EUI64).

View File

@ -122,6 +122,11 @@ message Tenant {
// do want to share uplinks with other tenants (private_gateways_up=false), // do want to share uplinks with other tenants (private_gateways_up=false),
// but you want to prevent other tenants from using gateway airtime. // but you want to prevent other tenants from using gateway airtime.
bool private_gateways_down = 8; bool private_gateways_down = 8;
// Tags (user defined).
// These tags can be used to add additional information to the tenant. These
// tags are NOT exposed in the integration events.
map<string, string> tags = 9;
} }
message TenantListItem { message TenantListItem {

View File

@ -0,0 +1,2 @@
alter table application drop column tags;
alter table tenant drop column tags;

View File

@ -0,0 +1,15 @@
alter table tenant
add column tags jsonb not null default '{}';
alter table tenant
alter column tags drop default;
create index idx_tenant_tags on tenant using gin (tags);
alter table application
add column tags jsonb not null default '{}';
alter table application
alter column tags drop default;
create index idx_application_tags on application using gin (tags);

View File

@ -10,7 +10,7 @@ use super::auth::validator;
use super::error::ToStatus; use super::error::ToStatus;
use super::helpers; use super::helpers;
use crate::certificate; use crate::certificate;
use crate::storage::application; use crate::storage::{application, fields};
pub struct Application { pub struct Application {
validator: validator::RequestValidator, validator: validator::RequestValidator,
@ -47,6 +47,7 @@ impl ApplicationService for Application {
tenant_id, tenant_id,
name: req_app.name.clone(), name: req_app.name.clone(),
description: req_app.description.clone(), description: req_app.description.clone(),
tags: fields::KeyValue::new(req_app.tags.clone()),
..Default::default() ..Default::default()
}; };
@ -86,6 +87,7 @@ impl ApplicationService for Application {
tenant_id: a.tenant_id.to_string(), tenant_id: a.tenant_id.to_string(),
name: a.name, name: a.name,
description: a.description, description: a.description,
tags: a.tags.into_hashmap(),
}), }),
created_at: Some(helpers::datetime_to_prost_timestamp(&a.created_at)), created_at: Some(helpers::datetime_to_prost_timestamp(&a.created_at)),
updated_at: Some(helpers::datetime_to_prost_timestamp(&a.updated_at)), updated_at: Some(helpers::datetime_to_prost_timestamp(&a.updated_at)),
@ -120,6 +122,7 @@ impl ApplicationService for Application {
id: app_id, id: app_id,
name: req_app.name.to_string(), name: req_app.name.to_string(),
description: req_app.description.to_string(), description: req_app.description.to_string(),
tags: fields::KeyValue::new(req_app.tags.clone()),
..Default::default() ..Default::default()
}) })
.await .await

View File

@ -9,7 +9,7 @@ use chirpstack_api::api::tenant_service_server::TenantService;
use super::auth::{validator, AuthID}; use super::auth::{validator, AuthID};
use super::error::ToStatus; use super::error::ToStatus;
use super::helpers; use super::helpers;
use crate::storage::{tenant, user}; use crate::storage::{fields, tenant, user};
pub struct Tenant { pub struct Tenant {
validator: validator::RequestValidator, validator: validator::RequestValidator,
@ -49,6 +49,7 @@ impl TenantService for Tenant {
max_gateway_count: req_tenant.max_gateway_count as i32, max_gateway_count: req_tenant.max_gateway_count as i32,
private_gateways_up: req_tenant.private_gateways_up, private_gateways_up: req_tenant.private_gateways_up,
private_gateways_down: req_tenant.private_gateways_down, private_gateways_down: req_tenant.private_gateways_down,
tags: fields::KeyValue::new(req_tenant.tags.clone()),
..Default::default() ..Default::default()
}; };
@ -89,6 +90,7 @@ impl TenantService for Tenant {
max_device_count: t.max_device_count as u32, max_device_count: t.max_device_count as u32,
private_gateways_up: t.private_gateways_up, private_gateways_up: t.private_gateways_up,
private_gateways_down: t.private_gateways_down, private_gateways_down: t.private_gateways_down,
tags: t.tags.into_hashmap(),
}), }),
created_at: Some(helpers::datetime_to_prost_timestamp(&t.created_at)), created_at: Some(helpers::datetime_to_prost_timestamp(&t.created_at)),
updated_at: Some(helpers::datetime_to_prost_timestamp(&t.updated_at)), updated_at: Some(helpers::datetime_to_prost_timestamp(&t.updated_at)),
@ -128,6 +130,7 @@ impl TenantService for Tenant {
max_gateway_count: req_tenant.max_gateway_count as i32, max_gateway_count: req_tenant.max_gateway_count as i32,
private_gateways_up: req_tenant.private_gateways_up, private_gateways_up: req_tenant.private_gateways_up,
private_gateways_down: req_tenant.private_gateways_down, private_gateways_down: req_tenant.private_gateways_down,
tags: fields::KeyValue::new(req_tenant.tags.clone()),
..Default::default() ..Default::default()
}) })
.await .await

View File

@ -443,7 +443,8 @@ impl Data {
device_class_enabled: self.device.enabled_class.to_proto().into(), device_class_enabled: self.device.enabled_class.to_proto().into(),
dev_eui: self.device.dev_eui.to_string(), dev_eui: self.device.dev_eui.to_string(),
tags: { tags: {
let mut tags = (*self.device_profile.tags).clone(); let mut tags = (*self.application.tags).clone();
tags.extend((*self.device_profile.tags).clone());
tags.extend((*self.device.tags).clone()); tags.extend((*self.device.tags).clone());
tags tags
}, },

View File

@ -441,7 +441,8 @@ impl TxAck {
let dp = self.device_profile.as_ref().unwrap(); let dp = self.device_profile.as_ref().unwrap();
let dev = self.device.as_ref().unwrap(); let dev = self.device.as_ref().unwrap();
let mut tags = (*dp.tags).clone(); let mut tags = (*app.tags).clone();
tags.extend((*dp.tags).clone());
tags.extend((*dev.tags).clone()); tags.extend((*dev.tags).clone());
let pl = integration_pb::LogEvent { let pl = integration_pb::LogEvent {

View File

@ -36,8 +36,9 @@ pub async fn handle(
) )
.await?; .await?;
let mut tags = (*dp.tags).clone(); let mut tags = (*app.tags).clone();
tags.clone_from(&*dev.tags); tags.extend((*dp.tags).clone());
tags.extend((*dev.tags).clone());
let rx_time: DateTime<Utc> = let rx_time: DateTime<Utc> =
helpers::get_rx_timestamp(&uplink_frame_set.rx_info_set).into(); helpers::get_rx_timestamp(&uplink_frame_set.rx_info_set).into();

View File

@ -36,7 +36,8 @@ pub async fn handle(
device_class_enabled: dev.enabled_class.to_proto().into(), device_class_enabled: dev.enabled_class.to_proto().into(),
dev_eui: dev.dev_eui.to_string(), dev_eui: dev.dev_eui.to_string(),
tags: { tags: {
let mut tags = (*dp.tags).clone(); let mut tags = (*app.tags).clone();
tags.extend((*dp.tags).clone());
tags.extend((*dev.tags).clone()); tags.extend((*dev.tags).clone());
tags tags
}, },

View File

@ -16,8 +16,8 @@ use tracing::info;
use uuid::Uuid; use uuid::Uuid;
use super::error::Error; use super::error::Error;
use super::get_db_conn;
use super::schema::{application, application_integration}; use super::schema::{application, application_integration};
use super::{fields, get_db_conn};
#[derive(Clone, Queryable, Insertable, PartialEq, Eq, Debug)] #[derive(Clone, Queryable, Insertable, PartialEq, Eq, Debug)]
#[diesel(table_name = application)] #[diesel(table_name = application)]
@ -29,6 +29,7 @@ pub struct Application {
pub name: String, pub name: String,
pub description: String, pub description: String,
pub mqtt_tls_cert: Option<Vec<u8>>, pub mqtt_tls_cert: Option<Vec<u8>>,
pub tags: fields::KeyValue,
} }
impl Application { impl Application {
@ -52,6 +53,7 @@ impl Default for Application {
name: "".into(), name: "".into(),
description: "".into(), description: "".into(),
mqtt_tls_cert: None, mqtt_tls_cert: None,
tags: fields::KeyValue::new(HashMap::new()),
} }
} }
} }
@ -328,6 +330,7 @@ pub async fn update(a: Application) -> Result<Application, Error> {
application::updated_at.eq(Utc::now()), application::updated_at.eq(Utc::now()),
application::name.eq(&a.name), application::name.eq(&a.name),
application::description.eq(&a.description), application::description.eq(&a.description),
application::tags.eq(&a.tags),
)) ))
.get_result(&mut c) .get_result(&mut c)
.map_err(|e| Error::from_diesel(e, a.id.to_string()))?; .map_err(|e| Error::from_diesel(e, a.id.to_string()))?;

View File

@ -195,9 +195,11 @@ pub async fn create(d: Device) -> Result<Device, Error> {
tenant::dsl::max_gateway_count, tenant::dsl::max_gateway_count,
tenant::dsl::private_gateways_up, tenant::dsl::private_gateways_up,
tenant::dsl::private_gateways_down, tenant::dsl::private_gateways_down,
tenant::dsl::tags,
)) ))
.inner_join(application::table) .inner_join(application::table)
.filter(application::dsl::id.eq(&d.application_id)) .filter(application::dsl::id.eq(&d.application_id))
.for_update()
.first(c)?; .first(c)?;
let dev_count: i64 = device::dsl::device let dev_count: i64 = device::dsl::device

View File

@ -21,6 +21,7 @@ diesel::table! {
name -> Varchar, name -> Varchar,
description -> Text, description -> Text,
mqtt_tls_cert -> Nullable<Bytea>, mqtt_tls_cert -> Nullable<Bytea>,
tags -> Jsonb,
} }
} }
@ -296,6 +297,7 @@ diesel::table! {
max_gateway_count -> Int4, max_gateway_count -> Int4,
private_gateways_up -> Bool, private_gateways_up -> Bool,
private_gateways_down -> Bool, private_gateways_down -> Bool,
tags -> Jsonb,
} }
} }

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use anyhow::Result; use anyhow::Result;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use diesel::dsl; use diesel::dsl;
@ -7,8 +9,8 @@ use tracing::info;
use uuid::Uuid; use uuid::Uuid;
use super::error::Error; use super::error::Error;
use super::get_db_conn;
use super::schema::{tenant, tenant_user, user}; use super::schema::{tenant, tenant_user, user};
use super::{fields, get_db_conn};
#[derive(Queryable, Insertable, PartialEq, Eq, Debug, Clone)] #[derive(Queryable, Insertable, PartialEq, Eq, Debug, Clone)]
#[diesel(table_name = tenant)] #[diesel(table_name = tenant)]
@ -23,6 +25,7 @@ pub struct Tenant {
pub max_gateway_count: i32, pub max_gateway_count: i32,
pub private_gateways_up: bool, pub private_gateways_up: bool,
pub private_gateways_down: bool, pub private_gateways_down: bool,
pub tags: fields::KeyValue,
} }
impl Tenant { impl Tenant {
@ -49,6 +52,7 @@ impl Default for Tenant {
max_gateway_count: 0, max_gateway_count: 0,
private_gateways_up: false, private_gateways_up: false,
private_gateways_down: false, private_gateways_down: false,
tags: fields::KeyValue::new(HashMap::new()),
} }
} }
} }
@ -145,6 +149,7 @@ pub async fn update(t: Tenant) -> Result<Tenant, Error> {
tenant::max_gateway_count.eq(&t.max_gateway_count), tenant::max_gateway_count.eq(&t.max_gateway_count),
tenant::private_gateways_up.eq(&t.private_gateways_up), tenant::private_gateways_up.eq(&t.private_gateways_up),
tenant::private_gateways_down.eq(&t.private_gateways_down), tenant::private_gateways_down.eq(&t.private_gateways_down),
tenant::tags.eq(&t.tags),
)) ))
.get_result(&mut c) .get_result(&mut c)
.map_err(|e| Error::from_diesel(e, t.id.to_string())) .map_err(|e| Error::from_diesel(e, t.id.to_string()))
@ -408,6 +413,7 @@ pub mod test {
max_gateway_count: 10, max_gateway_count: 10,
private_gateways_up: true, private_gateways_up: true,
private_gateways_down: true, private_gateways_down: true,
tags: fields::KeyValue::new(HashMap::new()),
}; };
create(t).await.unwrap() create(t).await.unwrap()
} }

View File

@ -387,7 +387,8 @@ impl Data {
let dp = self.device_profile.as_ref().unwrap(); let dp = self.device_profile.as_ref().unwrap();
let dev = self.device.as_ref().unwrap(); let dev = self.device.as_ref().unwrap();
let mut tags = (*dp.tags).clone(); let mut tags = (*app.tags).clone();
tags.extend((*dp.tags).clone());
tags.extend((*dev.tags).clone()); tags.extend((*dev.tags).clone());
self.device_info = Some(integration_pb::DeviceInfo { self.device_info = Some(integration_pb::DeviceInfo {
@ -1142,7 +1143,8 @@ impl Data {
device_queue::delete_item(&qi.id).await?; device_queue::delete_item(&qi.id).await?;
let mut tags = (*dp.tags).clone(); let mut tags = (*app.tags).clone();
tags.extend((*dp.tags).clone());
tags.extend((*dev.tags).clone()); tags.extend((*dev.tags).clone());
integration::ack_event( integration::ack_event(

View File

@ -317,7 +317,8 @@ impl JoinRequest {
let dp = self.device_profile.as_ref().unwrap(); let dp = self.device_profile.as_ref().unwrap();
let dev = self.device.as_ref().unwrap(); let dev = self.device.as_ref().unwrap();
let mut tags = (*dp.tags).clone(); let mut tags = (*app.tags).clone();
tags.extend((*dp.tags).clone());
tags.extend((*dev.tags).clone()); tags.extend((*dev.tags).clone());
self.device_info = Some(integration_pb::DeviceInfo { self.device_info = Some(integration_pb::DeviceInfo {

View File

@ -184,7 +184,8 @@ impl JoinRequest {
let dp = self.device_profile.as_ref().unwrap(); let dp = self.device_profile.as_ref().unwrap();
let dev = self.device.as_ref().unwrap(); let dev = self.device.as_ref().unwrap();
let mut tags = (*dp.tags).clone(); let mut tags = (*app.tags).clone();
tags.extend((*dp.tags).clone());
tags.extend((*dev.tags).clone()); tags.extend((*dev.tags).clone());
self.device_info = Some(integration_pb::DeviceInfo { self.device_info = Some(integration_pb::DeviceInfo {

View File

@ -1,5 +1,6 @@
import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb"; import { Application } from "@chirpstack/chirpstack-api-grpc-web/api/application_pb";
import { Form, Input, Button } from "antd"; import { Form, Input, Button, Tabs, Row, Col } from "antd";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
import { onFinishFailed } from "../helpers"; import { onFinishFailed } from "../helpers";
@ -19,17 +20,66 @@ function ApplicationForm(props: IProps) {
app.setName(v.name); app.setName(v.name);
app.setDescription(v.description); app.setDescription(v.description);
// tags
for (const elm of v.tagsMap) {
app.getTagsMap().set(elm[0], elm[1]);
}
props.onFinish(app); props.onFinish(app);
}; };
return ( return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} onFinishFailed={onFinishFailed}> <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} onFinishFailed={onFinishFailed}>
<Tabs>
<Tabs.TabPane tab="General" key="1">
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}> <Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
<Input disabled={props.disabled} /> <Input disabled={props.disabled} />
</Form.Item> </Form.Item>
<Form.Item label="Description" name="description"> <Form.Item label="Description" name="description">
<Input.TextArea disabled={props.disabled} /> <Input.TextArea disabled={props.disabled} />
</Form.Item> </Form.Item>
</Tabs.TabPane>
<Tabs.TabPane tab="Tags" key="2">
<Form.List name="tagsMap">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Row gutter={24}>
<Col span={6}>
<Form.Item
{...restField}
name={[name, 0]}
fieldKey={[name, 0]}
rules={[{ required: true, message: "Please enter a key!" }]}
>
<Input placeholder="Key" />
</Form.Item>
</Col>
<Col span={16}>
<Form.Item
{...restField}
name={[name, 1]}
fieldKey={[name, 1]}
rules={[{ required: true, message: "Please enter a value!" }]}
>
<Input placeholder="Value" />
</Form.Item>
</Col>
<Col span={2}>
<MinusCircleOutlined onClick={() => remove(name)} />
</Col>
</Row>
))}
<Form.Item>
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
Add tag
</Button>
</Form.Item>
</>
)}
</Form.List>
</Tabs.TabPane>
</Tabs>
<Form.Item> <Form.Item>
<Button type="primary" htmlType="submit" disabled={props.disabled}> <Button type="primary" htmlType="submit" disabled={props.disabled}>
Submit Submit

View File

@ -1,4 +1,5 @@
import { Form, Input, InputNumber, Switch, Row, Col, Button } from "antd"; import { Form, Input, InputNumber, Switch, Row, Col, Button, Tabs } from "antd";
import { MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb"; import { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
@ -24,11 +25,18 @@ function TenantForm(props: IProps) {
tenant.setPrivateGatewaysUp(values.privateGatewaysUp); tenant.setPrivateGatewaysUp(values.privateGatewaysUp);
tenant.setPrivateGatewaysDown(values.privateGatewaysDown); tenant.setPrivateGatewaysDown(values.privateGatewaysDown);
// tags
for (const elm of v.tagsMap) {
tenant.getTagsMap().set(elm[0], elm[1]);
}
props.onFinish(tenant); props.onFinish(tenant);
}; };
return ( return (
<Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} onFinishFailed={onFinishFailed}> <Form layout="vertical" initialValues={props.initialValues.toObject()} onFinish={onFinish} onFinishFailed={onFinishFailed}>
<Tabs>
<Tabs.TabPane tab="General" key="1">
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}> <Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
<Input disabled={props.disabled} /> <Input disabled={props.disabled} />
</Form.Item> </Form.Item>
@ -88,6 +96,49 @@ function TenantForm(props: IProps) {
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>
</Tabs.TabPane>
<Tabs.TabPane tab="Tags" key="2">
<Form.List name="tagsMap">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Row gutter={24}>
<Col span={6}>
<Form.Item
{...restField}
name={[name, 0]}
fieldKey={[name, 0]}
rules={[{ required: true, message: "Please enter a key!" }]}
>
<Input placeholder="Key" disabled={props.disabled} />
</Form.Item>
</Col>
<Col span={16}>
<Form.Item
{...restField}
name={[name, 1]}
fieldKey={[name, 1]}
rules={[{ required: true, message: "Please enter a value!" }]}
>
<Input placeholder="Value" disabled={props.disabled} />
</Form.Item>
</Col>
{!props.disabled &&
<Col span={2}>
<MinusCircleOutlined onClick={() => remove(name)} />
</Col>}
</Row>
))}
{!props.disabled && <Form.Item>
<Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
Add tag
</Button>
</Form.Item>}
</>
)}
</Form.List>
</Tabs.TabPane>
</Tabs>
<Form.Item> <Form.Item>
<Button type="primary" htmlType="submit" disabled={props.disabled}> <Button type="primary" htmlType="submit" disabled={props.disabled}>
Submit Submit