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).
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 {

View File

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

View File

@ -28,7 +28,8 @@ enum MeasurementKind {
// Unknown (in which case it is not tracked).
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;
// Counters that do get reset upon reading.
@ -79,7 +80,6 @@ enum SecondChAckOffset {
// 3200 kHz.
KHZ_3200 = 5;
}
enum RelayModeActivation {
@ -96,49 +96,51 @@ enum RelayModeActivation {
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 {
// Create the given device-profile.
rpc Create(CreateDeviceProfileRequest) returns (CreateDeviceProfileResponse) {
option(google.api.http) = {
post: "/api/device-profiles"
body: "*"
option (google.api.http) = {
post : "/api/device-profiles"
body : "*"
};
}
// Get the device-profile for the given ID.
rpc Get(GetDeviceProfileRequest) returns (GetDeviceProfileResponse) {
option(google.api.http) = {
get: "/api/device-profiles/{id}"
option (google.api.http) = {
get : "/api/device-profiles/{id}"
};
}
// Update the given device-profile.
rpc Update(UpdateDeviceProfileRequest) returns (google.protobuf.Empty) {
option(google.api.http) = {
put: "/api/device-profiles/{device_profile.id}"
body: "*"
option (google.api.http) = {
put : "/api/device-profiles/{device_profile.id}"
body : "*"
};
}
// Delete the device-profile with the given ID.
rpc Delete(DeleteDeviceProfileRequest) returns (google.protobuf.Empty) {
option(google.api.http) = {
delete: "/api/device-profiles/{id}"
option (google.api.http) = {
delete : "/api/device-profiles/{id}"
};
}
// List the available device-profiles.
rpc List(ListDeviceProfilesRequest) returns (ListDeviceProfilesResponse) {
option(google.api.http) = {
get: "/api/device-profiles"
option (google.api.http) = {
get : "/api/device-profiles"
};
}
// List available ADR algorithms.
rpc ListAdrAlgorithms(google.protobuf.Empty) returns (ListDeviceProfileAdrAlgorithmsResponse) {
option(google.api.http) = {
get: "/api/device-profiles/adr-algorithms"
rpc ListAdrAlgorithms(google.protobuf.Empty)
returns (ListDeviceProfileAdrAlgorithmsResponse) {
option (google.api.http) = {
get : "/api/device-profiles/adr-algorithms"
};
}
}
@ -185,8 +187,8 @@ message DeviceProfile {
uint32 uplink_interval = 11;
// Device-status request interval (times / day).
// This defines the times per day that ChirpStack will request the device-status
// from the device.
// This defines the times per day that ChirpStack will request the
// device-status from the device.
uint32 device_status_req_interval = 12;
// Supports OTAA.
@ -199,7 +201,8 @@ message DeviceProfile {
bool supports_class_c = 15;
// 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;
// Class-B ping-slots per beacon period.
@ -215,7 +218,8 @@ message DeviceProfile {
uint32 class_b_ping_slot_freq = 19;
// 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;
// RX1 delay (for ABP).
@ -230,7 +234,10 @@ message DeviceProfile {
// RX2 frequency (for ABP, Hz).
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;
// Measurements.

View File

@ -16,76 +16,76 @@ import "google/protobuf/empty.proto";
service TenantService {
// Create a new tenant.
rpc Create(CreateTenantRequest) returns (CreateTenantResponse) {
option(google.api.http) = {
post: "/api/tenants"
body: "*"
option (google.api.http) = {
post : "/api/tenants"
body : "*"
};
}
// Get the tenant for the given ID.
rpc Get(GetTenantRequest) returns (GetTenantResponse) {
option(google.api.http) = {
get: "/api/tenants/{id}"
option (google.api.http) = {
get : "/api/tenants/{id}"
};
}
// Update the given tenant.
rpc Update(UpdateTenantRequest) returns (google.protobuf.Empty) {
option(google.api.http) = {
put: "/api/tenants/{tenant.id}"
body: "*"
option (google.api.http) = {
put : "/api/tenants/{tenant.id}"
body : "*"
};
}
// Delete the tenant with the given ID.
rpc Delete(DeleteTenantRequest) returns (google.protobuf.Empty) {
option(google.api.http) = {
delete: "/api/tenants/{id}"
option (google.api.http) = {
delete : "/api/tenants/{id}"
};
}
// Get the list of tenants.
rpc List(ListTenantsRequest) returns (ListTenantsResponse) {
option(google.api.http) = {
get: "/api/tenants"
option (google.api.http) = {
get : "/api/tenants"
};
}
// Add an user to the tenant.
// Note: the user must already exist.
rpc AddUser(AddTenantUserRequest) returns (google.protobuf.Empty) {
option(google.api.http) = {
post: "/api/tenants/{tenant_user.tenant_id}/users"
body: "*"
option (google.api.http) = {
post : "/api/tenants/{tenant_user.tenant_id}/users"
body : "*"
};
}
// Get the the tenant user for the given tenant and user IDs.
rpc GetUser(GetTenantUserRequest) returns (GetTenantUserResponse) {
option(google.api.http) = {
get: "/api/tenants/{tenant_id}/users/{user_id}"
option (google.api.http) = {
get : "/api/tenants/{tenant_id}/users/{user_id}"
};
}
// Update the given tenant user.
rpc UpdateUser(UpdateTenantUserRequest) returns (google.protobuf.Empty) {
option(google.api.http) = {
put: "/api/tenants/{tenant_user.tenant_id}/users/{tenant_user.user_id}"
body: "*"
option (google.api.http) = {
put : "/api/tenants/{tenant_user.tenant_id}/users/{tenant_user.user_id}"
body : "*"
};
}
// Delete the given tenant user.
rpc DeleteUser(DeleteTenantUserRequest) returns (google.protobuf.Empty) {
option(google.api.http) = {
delete: "/api/tenants/{tenant_id}/users/{user_id}"
option (google.api.http) = {
delete : "/api/tenants/{tenant_id}/users/{user_id}"
};
}
// Get the list of tenant users.
rpc ListUsers(ListTenantUsersRequest) returns (ListTenantUsersResponse) {
option(google.api.http) = {
get: "/api/tenants/{tenant_id}/users"
option (google.api.http) = {
get : "/api/tenants/{tenant_id}/users"
};
}
}
@ -122,6 +122,11 @@ message Tenant {
// do want to share uplinks with other tenants (private_gateways_up=false),
// but you want to prevent other tenants from using gateway airtime.
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 {

View File

@ -459,6 +459,12 @@ message Application {
// Tenant ID (UUID).
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 {

View File

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

View File

@ -16,76 +16,76 @@ import "google/protobuf/empty.proto";
service TenantService {
// Create a new tenant.
rpc Create(CreateTenantRequest) returns (CreateTenantResponse) {
option(google.api.http) = {
post: "/api/tenants"
body: "*"
option (google.api.http) = {
post : "/api/tenants"
body : "*"
};
}
// Get the tenant for the given ID.
rpc Get(GetTenantRequest) returns (GetTenantResponse) {
option(google.api.http) = {
get: "/api/tenants/{id}"
option (google.api.http) = {
get : "/api/tenants/{id}"
};
}
// Update the given tenant.
rpc Update(UpdateTenantRequest) returns (google.protobuf.Empty) {
option(google.api.http) = {
put: "/api/tenants/{tenant.id}"
body: "*"
option (google.api.http) = {
put : "/api/tenants/{tenant.id}"
body : "*"
};
}
// Delete the tenant with the given ID.
rpc Delete(DeleteTenantRequest) returns (google.protobuf.Empty) {
option(google.api.http) = {
delete: "/api/tenants/{id}"
option (google.api.http) = {
delete : "/api/tenants/{id}"
};
}
// Get the list of tenants.
rpc List(ListTenantsRequest) returns (ListTenantsResponse) {
option(google.api.http) = {
get: "/api/tenants"
option (google.api.http) = {
get : "/api/tenants"
};
}
// Add an user to the tenant.
// Note: the user must already exist.
rpc AddUser(AddTenantUserRequest) returns (google.protobuf.Empty) {
option(google.api.http) = {
post: "/api/tenants/{tenant_user.tenant_id}/users"
body: "*"
option (google.api.http) = {
post : "/api/tenants/{tenant_user.tenant_id}/users"
body : "*"
};
}
// Get the the tenant user for the given tenant and user IDs.
rpc GetUser(GetTenantUserRequest) returns (GetTenantUserResponse) {
option(google.api.http) = {
get: "/api/tenants/{tenant_id}/users/{user_id}"
option (google.api.http) = {
get : "/api/tenants/{tenant_id}/users/{user_id}"
};
}
// Update the given tenant user.
rpc UpdateUser(UpdateTenantUserRequest) returns (google.protobuf.Empty) {
option(google.api.http) = {
put: "/api/tenants/{tenant_user.tenant_id}/users/{tenant_user.user_id}"
body: "*"
option (google.api.http) = {
put : "/api/tenants/{tenant_user.tenant_id}/users/{tenant_user.user_id}"
body : "*"
};
}
// Delete the given tenant user.
rpc DeleteUser(DeleteTenantUserRequest) returns (google.protobuf.Empty) {
option(google.api.http) = {
delete: "/api/tenants/{tenant_id}/users/{user_id}"
option (google.api.http) = {
delete : "/api/tenants/{tenant_id}/users/{user_id}"
};
}
// Get the list of tenant users.
rpc ListUsers(ListTenantUsersRequest) returns (ListTenantUsersResponse) {
option(google.api.http) = {
get: "/api/tenants/{tenant_id}/users"
option (google.api.http) = {
get : "/api/tenants/{tenant_id}/users"
};
}
}
@ -122,6 +122,11 @@ message Tenant {
// do want to share uplinks with other tenants (private_gateways_up=false),
// but you want to prevent other tenants from using gateway airtime.
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 {

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

View File

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

View File

@ -443,7 +443,8 @@ impl Data {
device_class_enabled: self.device.enabled_class.to_proto().into(),
dev_eui: self.device.dev_eui.to_string(),
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
},

View File

@ -441,7 +441,8 @@ impl TxAck {
let dp = self.device_profile.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());
let pl = integration_pb::LogEvent {

View File

@ -36,8 +36,9 @@ pub async fn handle(
)
.await?;
let mut tags = (*dp.tags).clone();
tags.clone_from(&*dev.tags);
let mut tags = (*app.tags).clone();
tags.extend((*dp.tags).clone());
tags.extend((*dev.tags).clone());
let rx_time: DateTime<Utc> =
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(),
dev_eui: dev.dev_eui.to_string(),
tags: {
let mut tags = (*dp.tags).clone();
let mut tags = (*app.tags).clone();
tags.extend((*dp.tags).clone());
tags.extend((*dev.tags).clone());
tags
},

View File

@ -16,8 +16,8 @@ use tracing::info;
use uuid::Uuid;
use super::error::Error;
use super::get_db_conn;
use super::schema::{application, application_integration};
use super::{fields, get_db_conn};
#[derive(Clone, Queryable, Insertable, PartialEq, Eq, Debug)]
#[diesel(table_name = application)]
@ -29,6 +29,7 @@ pub struct Application {
pub name: String,
pub description: String,
pub mqtt_tls_cert: Option<Vec<u8>>,
pub tags: fields::KeyValue,
}
impl Application {
@ -52,6 +53,7 @@ impl Default for Application {
name: "".into(),
description: "".into(),
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::name.eq(&a.name),
application::description.eq(&a.description),
application::tags.eq(&a.tags),
))
.get_result(&mut c)
.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::private_gateways_up,
tenant::dsl::private_gateways_down,
tenant::dsl::tags,
))
.inner_join(application::table)
.filter(application::dsl::id.eq(&d.application_id))
.for_update()
.first(c)?;
let dev_count: i64 = device::dsl::device

View File

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

View File

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

View File

@ -387,7 +387,8 @@ impl Data {
let dp = self.device_profile.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());
self.device_info = Some(integration_pb::DeviceInfo {
@ -1142,7 +1143,8 @@ impl Data {
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());
integration::ack_event(

View File

@ -317,7 +317,8 @@ impl JoinRequest {
let dp = self.device_profile.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());
self.device_info = Some(integration_pb::DeviceInfo {

View File

@ -184,7 +184,8 @@ impl JoinRequest {
let dp = self.device_profile.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());
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 { 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";
@ -19,17 +20,66 @@ function ApplicationForm(props: IProps) {
app.setName(v.name);
app.setDescription(v.description);
// tags
for (const elm of v.tagsMap) {
app.getTagsMap().set(elm[0], elm[1]);
}
props.onFinish(app);
};
return (
<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!" }]}>
<Input disabled={props.disabled} />
</Form.Item>
<Form.Item label="Description" name="description">
<Input.TextArea disabled={props.disabled} />
</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>
<Button type="primary" htmlType="submit" disabled={props.disabled}>
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";
@ -24,11 +25,18 @@ function TenantForm(props: IProps) {
tenant.setPrivateGatewaysUp(values.privateGatewaysUp);
tenant.setPrivateGatewaysDown(values.privateGatewaysDown);
// tags
for (const elm of v.tagsMap) {
tenant.getTagsMap().set(elm[0], elm[1]);
}
props.onFinish(tenant);
};
return (
<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!" }]}>
<Input disabled={props.disabled} />
</Form.Item>
@ -88,6 +96,49 @@ function TenantForm(props: IProps) {
</Form.Item>
</Col>
</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>
<Button type="primary" htmlType="submit" disabled={props.disabled}>
Submit