@dbs-portal/module-tenant-management
Version:
Tenant management and multi-tenancy support module
186 lines • 14.4 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
/**
* TenantFeatureManager Component - Manage tenant features and capabilities
*/
import React, { useState, useEffect } from 'react';
import { Card, List, Switch, Button, Space, Typography, Tag, Modal, Form, Input, Select, DatePicker, Alert, Tooltip, Badge, Divider, Row, Col, Collapse, InputNumber } from 'antd';
import { SettingOutlined, EditOutlined, SaveOutlined, ReloadOutlined, InfoCircleOutlined, ExclamationCircleOutlined, CheckCircleOutlined, ClockCircleOutlined } from '@ant-design/icons';
import { format, isAfter, isBefore } from 'date-fns';
import { useTenant, useAvailableFeatures, useUpdateTenantFeatures } from '../hooks';
const { Title, Text, Paragraph } = Typography;
const { Option } = Select;
const { Panel } = Collapse;
const { TextArea } = Input;
export const TenantFeatureManager = ({ tenantId, onFeatureUpdate, className }) => {
const [editingFeature, setEditingFeature] = useState(null);
const [showAddModal, setShowAddModal] = useState(false);
const [form] = Form.useForm();
// Fetch data
const { data: tenant, isLoading: tenantLoading } = useTenant(tenantId);
const { data: availableFeatures = [], isLoading: featuresLoading } = useAvailableFeatures();
const updateFeaturesMutation = useUpdateTenantFeatures();
const isLoading = tenantLoading || featuresLoading;
// Get current tenant features
const currentFeatures = tenant?.features || [];
// Get available features that are not yet enabled
const unenabledFeatures = availableFeatures.filter(available => !currentFeatures.some(current => current.name === available.name));
const handleFeatureToggle = async (feature, enabled) => {
try {
const updatedFeatures = currentFeatures.map(f => f.name === feature.name ? { ...f, isEnabled: enabled } : f);
await updateFeaturesMutation.mutateAsync({
id: tenantId,
features: updatedFeatures
});
onFeatureUpdate?.({ ...feature, isEnabled: enabled });
}
catch (error) {
console.error('Failed to toggle feature:', error);
}
};
const handleFeatureEdit = (feature) => {
setEditingFeature(feature);
form.setFieldsValue({
name: feature.name,
isEnabled: feature.isEnabled,
value: feature.value,
expiresAt: feature.expiresAt ? new Date(feature.expiresAt) : undefined
});
};
const handleFeatureUpdate = async (values) => {
if (!editingFeature)
return;
try {
const updatedFeatures = currentFeatures.map(f => f.name === editingFeature.name
? {
...f,
isEnabled: values.isEnabled,
value: values.value,
expiresAt: values.expiresAt
}
: f);
await updateFeaturesMutation.mutateAsync({
id: tenantId,
features: updatedFeatures
});
const updatedFeature = {
...editingFeature,
isEnabled: values.isEnabled,
value: values.value,
expiresAt: values.expiresAt
};
onFeatureUpdate?.(updatedFeature);
setEditingFeature(null);
form.resetFields();
}
catch (error) {
console.error('Failed to update feature:', error);
}
};
const handleAddFeature = async (values) => {
try {
const availableFeature = availableFeatures.find(f => f.name === values.name);
if (!availableFeature)
return;
const newFeature = {
name: values.name,
displayName: availableFeature.displayName,
isEnabled: values.isEnabled,
value: values.value,
expiresAt: values.expiresAt
};
const updatedFeatures = [...currentFeatures, newFeature];
await updateFeaturesMutation.mutateAsync({
id: tenantId,
features: updatedFeatures
});
onFeatureUpdate?.(newFeature);
setShowAddModal(false);
form.resetFields();
}
catch (error) {
console.error('Failed to add feature:', error);
}
};
const getFeatureStatus = (feature) => {
if (!feature.isEnabled) {
return { status: 'disabled', color: 'default', icon: _jsx(ExclamationCircleOutlined, {}) };
}
if (feature.expiresAt) {
const now = new Date();
const expiryDate = new Date(feature.expiresAt);
if (isAfter(now, expiryDate)) {
return { status: 'expired', color: 'error', icon: _jsx(ClockCircleOutlined, {}) };
}
if (isBefore(expiryDate, new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000))) {
return { status: 'expiring', color: 'warning', icon: _jsx(ClockCircleOutlined, {}) };
}
}
return { status: 'active', color: 'success', icon: _jsx(CheckCircleOutlined, {}) };
};
const renderFeatureValue = (feature, availableFeature) => {
if (!feature.value)
return _jsx(Text, { type: "secondary", children: "No value" });
const valueType = availableFeature?.valueType || 'string';
switch (valueType) {
case 'boolean':
return _jsx(Tag, { color: feature.value ? 'success' : 'default', children: feature.value ? 'Yes' : 'No' });
case 'number':
return _jsx(Text, { code: true, children: feature.value.toLocaleString() });
case 'object':
return (_jsx(Tooltip, { title: JSON.stringify(feature.value, null, 2), children: _jsxs(Text, { code: true, children: ["Object (", Object.keys(feature.value).length, " keys)"] }) }));
default:
return _jsx(Text, { code: true, children: String(feature.value) });
}
};
const renderValueInput = (availableFeature) => {
switch (availableFeature.valueType) {
case 'boolean':
return (_jsx(Form.Item, { name: "value", valuePropName: "checked", children: _jsx(Switch, {}) }));
case 'number':
return (_jsx(Form.Item, { name: "value", children: _jsx(InputNumber, { style: { width: '100%' } }) }));
case 'object':
return (_jsx(Form.Item, { name: "value", children: _jsx(TextArea, { rows: 4, placeholder: "Enter JSON object", onChange: (e) => {
try {
const parsed = JSON.parse(e.target.value);
form.setFieldValue('value', parsed);
}
catch {
// Invalid JSON, keep as string
}
} }) }));
default:
return (_jsx(Form.Item, { name: "value", children: _jsx(Input, {}) }));
}
};
if (isLoading) {
return (_jsx(Card, { className: className, loading: true, children: _jsx("div", { style: { height: 300 } }) }));
}
return (_jsxs("div", { className: className, children: [_jsx(Card, { title: _jsxs(Space, { children: [_jsx(SettingOutlined, {}), _jsx(Title, { level: 4, style: { margin: 0 }, children: "Feature Management" })] }), extra: _jsxs(Space, { children: [_jsx(Button, { icon: _jsx(ReloadOutlined, {}), onClick: () => window.location.reload(), children: "Refresh" }), unenabledFeatures.length > 0 && (_jsx(Button, { type: "primary", icon: _jsx(SettingOutlined, {}), onClick: () => setShowAddModal(true), children: "Add Feature" }))] }), children: currentFeatures.length === 0 ? (_jsx(Alert, { message: "No features configured", description: "This tenant has no features enabled. Add features to enhance functionality.", type: "info", showIcon: true, action: unenabledFeatures.length > 0 && (_jsx(Button, { type: "primary", onClick: () => setShowAddModal(true), children: "Add Feature" })) })) : (_jsx(List, { dataSource: currentFeatures, renderItem: (feature) => {
const availableFeature = availableFeatures.find(f => f.name === feature.name);
const status = getFeatureStatus(feature);
return (_jsx(List.Item, { actions: [
_jsx(Switch, { checked: feature.isEnabled, onChange: (checked) => handleFeatureToggle(feature, checked), loading: updateFeaturesMutation.isPending }, "toggle"),
_jsx(Button, { icon: _jsx(EditOutlined, {}), size: "small", onClick: () => handleFeatureEdit(feature), children: "Edit" }, "edit")
], children: _jsx(List.Item.Meta, { avatar: _jsx(Badge, { status: status.color, children: status.icon }), title: _jsxs(Space, { children: [_jsx(Text, { strong: true, children: feature.displayName }), _jsx(Tag, { color: status.color, children: status.status.toUpperCase() }), availableFeature?.isRequired && (_jsx(Tag, { color: "red", children: "REQUIRED" }))] }), description: _jsxs(Space, { direction: "vertical", size: "small", children: [_jsx(Text, { type: "secondary", children: feature.name }), availableFeature?.description && (_jsx(Text, { children: availableFeature.description })), _jsxs(Space, { children: [_jsx(Text, { strong: true, children: "Value:" }), renderFeatureValue(feature, availableFeature)] }), feature.expiresAt && (_jsxs(Space, { children: [_jsx(Text, { strong: true, children: "Expires:" }), _jsx(Text, { type: status.status === 'expired' ? 'danger' : 'secondary', children: format(new Date(feature.expiresAt), 'PPP') })] }))] }) }) }));
} })) }), availableFeatures.length > 0 && (_jsx(Card, { style: { marginTop: 16 }, title: "Available Features", children: _jsx(Collapse, { ghost: true, children: _jsx(Panel, { header: `View all available features (${availableFeatures.length})`, children: _jsx(List, { dataSource: availableFeatures, renderItem: (feature) => (_jsx(List.Item, { children: _jsx(List.Item.Meta, { title: _jsxs(Space, { children: [_jsx(Text, { strong: true, children: feature.displayName }), _jsx(Tag, { children: feature.category }), feature.isRequired && _jsx(Tag, { color: "red", children: "REQUIRED" })] }), description: _jsxs(Space, { direction: "vertical", size: "small", children: [_jsx(Text, { children: feature.description }), _jsxs(Space, { children: [_jsx(Text, { strong: true, children: "Type:" }), _jsx(Tag, { color: "blue", children: feature.valueType }), feature.defaultValue && (_jsxs(_Fragment, { children: [_jsx(Text, { strong: true, children: "Default:" }), _jsx(Text, { code: true, children: JSON.stringify(feature.defaultValue) })] }))] })] }) }) })) }) }, "1") }) })), _jsx(Modal, { title: "Edit Feature", open: !!editingFeature, onCancel: () => {
setEditingFeature(null);
form.resetFields();
}, footer: null, width: 600, children: editingFeature && (_jsxs(Form, { form: form, layout: "vertical", onFinish: handleFeatureUpdate, children: [_jsx(Form.Item, { label: "Feature Name", children: _jsx(Input, { value: editingFeature.displayName, disabled: true }) }), _jsxs(Form.Item, { name: "isEnabled", valuePropName: "checked", children: [_jsx(Switch, {}), " Enable this feature"] }), _jsx(Form.Item, { label: "Value", children: renderValueInput(availableFeatures.find(f => f.name === editingFeature.name) || {
name: editingFeature.name,
displayName: editingFeature.displayName,
description: '',
category: '',
valueType: 'string',
isRequired: false
}) }), _jsx(Form.Item, { name: "expiresAt", label: "Expiry Date", children: _jsx(DatePicker, { showTime: true, style: { width: '100%' } }) }), _jsxs(Row, { justify: "end", gutter: 8, children: [_jsx(Col, { children: _jsx(Button, { onClick: () => {
setEditingFeature(null);
form.resetFields();
}, children: "Cancel" }) }), _jsx(Col, { children: _jsx(Button, { type: "primary", htmlType: "submit", icon: _jsx(SaveOutlined, {}), loading: updateFeaturesMutation.isPending, children: "Save Changes" }) })] })] })) }), _jsx(Modal, { title: "Add Feature", open: showAddModal, onCancel: () => {
setShowAddModal(false);
form.resetFields();
}, footer: null, width: 600, children: _jsxs(Form, { form: form, layout: "vertical", onFinish: handleAddFeature, children: [_jsx(Form.Item, { name: "name", label: "Feature", rules: [{ required: true, message: 'Please select a feature' }], children: _jsx(Select, { placeholder: "Select a feature to add", children: unenabledFeatures.map(feature => (_jsx(Option, { value: feature.name, children: _jsxs(Space, { children: [_jsx(Text, { strong: true, children: feature.displayName }), _jsx(Tag, { children: feature.category }), feature.isRequired && _jsx(Tag, { color: "red", children: "REQUIRED" })] }) }, feature.name))) }) }), _jsxs(Form.Item, { name: "isEnabled", valuePropName: "checked", initialValue: true, children: [_jsx(Switch, {}), " Enable this feature"] }), _jsx(Form.Item, { label: "Value", children: _jsx(Text, { type: "secondary", children: "Value input will appear based on selected feature type" }) }), _jsx(Form.Item, { name: "expiresAt", label: "Expiry Date", children: _jsx(DatePicker, { showTime: true, style: { width: '100%' } }) }), _jsxs(Row, { justify: "end", gutter: 8, children: [_jsx(Col, { children: _jsx(Button, { onClick: () => {
setShowAddModal(false);
form.resetFields();
}, children: "Cancel" }) }), _jsx(Col, { children: _jsx(Button, { type: "primary", htmlType: "submit", icon: _jsx(SaveOutlined, {}), loading: updateFeaturesMutation.isPending, children: "Add Feature" }) })] })] }) })] }));
};
//# sourceMappingURL=TenantFeatureManager.js.map