@dashadmin/dash-admin 1.3.70 → 1.3.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/tenant/SettingsSections.js +77 -0
- package/dist/components/tenant/TenancySettings.js +98 -0
- package/dist/components/tenant/TenantSettings.js +3 -72
- package/dist/systemResources.js +216 -1
- package/package.json +14 -14
- package/src/components/tenant/SettingsSections.tsx +151 -0
- package/src/components/tenant/TenancySettings.tsx +122 -0
- package/src/components/tenant/TenantSettings.tsx +8 -139
- package/src/systemResources.tsx +223 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { DashAutoFormTabs } from "dash-auto-admin";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useTranslate } from "react-admin";
|
|
5
|
+
import {
|
|
6
|
+
Box,
|
|
7
|
+
Card,
|
|
8
|
+
CardActionArea,
|
|
9
|
+
CardContent,
|
|
10
|
+
Divider,
|
|
11
|
+
Drawer,
|
|
12
|
+
IconButton,
|
|
13
|
+
Typography
|
|
14
|
+
} from "@mui/material";
|
|
15
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
16
|
+
const FormTabsRenderer = ({ schema, method, label }) => /* @__PURE__ */ jsx(Fragment, { children: DashAutoFormTabs({ schema, resourceConfig: null, options: { mode: method, label } }) });
|
|
17
|
+
const toSettingsSections = (schema) => {
|
|
18
|
+
const order = [];
|
|
19
|
+
const byGroup = {};
|
|
20
|
+
schema.forEach((entry) => {
|
|
21
|
+
const key = entry.group || entry.tab || "general";
|
|
22
|
+
if (!byGroup[key]) {
|
|
23
|
+
byGroup[key] = [];
|
|
24
|
+
order.push(key);
|
|
25
|
+
}
|
|
26
|
+
byGroup[key].push(entry);
|
|
27
|
+
});
|
|
28
|
+
return order.map((key) => ({ key, entries: byGroup[key] }));
|
|
29
|
+
};
|
|
30
|
+
const prettifyGroup = (key) => key.replace(/[_-]+/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
31
|
+
const SettingsSections = ({ schema, method, translationNamespace = "tenant.settings" }) => {
|
|
32
|
+
const translate = useTranslate();
|
|
33
|
+
const [openSection, setOpenSection] = useState(null);
|
|
34
|
+
const sections = toSettingsSections(schema);
|
|
35
|
+
const labelFor = (key) => translate(`${translationNamespace}.groups.${key}`, { _: prettifyGroup(key) });
|
|
36
|
+
return /* @__PURE__ */ jsxs("section", { children: [
|
|
37
|
+
/* @__PURE__ */ jsx(
|
|
38
|
+
Box,
|
|
39
|
+
{
|
|
40
|
+
sx: {
|
|
41
|
+
display: "grid",
|
|
42
|
+
gap: 2,
|
|
43
|
+
gridTemplateColumns: { xs: "1fr", sm: "repeat(2, 1fr)", md: "repeat(3, 1fr)" }
|
|
44
|
+
},
|
|
45
|
+
children: sections.map((section) => /* @__PURE__ */ jsx(Card, { variant: "outlined", children: /* @__PURE__ */ jsx(CardActionArea, { onClick: () => setOpenSection(section.key), children: /* @__PURE__ */ jsxs(CardContent, { children: [
|
|
46
|
+
/* @__PURE__ */ jsx(Typography, { variant: "subtitle1", sx: { fontWeight: 600 }, children: labelFor(section.key) }),
|
|
47
|
+
/* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.secondary", children: translate(`${translationNamespace}.option_count`, {
|
|
48
|
+
smart_count: section.entries.length,
|
|
49
|
+
_: `${section.entries.length}`
|
|
50
|
+
}) })
|
|
51
|
+
] }) }) }, section.key))
|
|
52
|
+
}
|
|
53
|
+
),
|
|
54
|
+
sections.map((section) => /* @__PURE__ */ jsx(
|
|
55
|
+
Drawer,
|
|
56
|
+
{
|
|
57
|
+
anchor: "right",
|
|
58
|
+
open: openSection === section.key,
|
|
59
|
+
onClose: () => setOpenSection(null),
|
|
60
|
+
ModalProps: { keepMounted: true },
|
|
61
|
+
children: /* @__PURE__ */ jsxs(Box, { sx: { width: { xs: "100vw", sm: 560 }, p: 2 }, children: [
|
|
62
|
+
/* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", mb: 1 }, children: [
|
|
63
|
+
/* @__PURE__ */ jsx(Typography, { variant: "h6", children: labelFor(section.key) }),
|
|
64
|
+
/* @__PURE__ */ jsx(IconButton, { onClick: () => setOpenSection(null), size: "small", "aria-label": "Close", children: /* @__PURE__ */ jsx(CloseIcon, {}) })
|
|
65
|
+
] }),
|
|
66
|
+
/* @__PURE__ */ jsx(Divider, { sx: { mb: 2 } }),
|
|
67
|
+
/* @__PURE__ */ jsx(FormTabsRenderer, { schema: section.entries, method, label: labelFor(section.key) })
|
|
68
|
+
] })
|
|
69
|
+
},
|
|
70
|
+
section.key
|
|
71
|
+
))
|
|
72
|
+
] });
|
|
73
|
+
};
|
|
74
|
+
export {
|
|
75
|
+
FormTabsRenderer,
|
|
76
|
+
SettingsSections
|
|
77
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defProps = Object.defineProperties;
|
|
3
|
+
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
|
|
4
|
+
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __propIsEnum = Object.prototype.propertyIsEnumerable;
|
|
7
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
8
|
+
var __spreadValues = (a, b) => {
|
|
9
|
+
for (var prop in b || (b = {}))
|
|
10
|
+
if (__hasOwnProp.call(b, prop))
|
|
11
|
+
__defNormalProp(a, prop, b[prop]);
|
|
12
|
+
if (__getOwnPropSymbols)
|
|
13
|
+
for (var prop of __getOwnPropSymbols(b)) {
|
|
14
|
+
if (__propIsEnum.call(b, prop))
|
|
15
|
+
__defNormalProp(a, prop, b[prop]);
|
|
16
|
+
}
|
|
17
|
+
return a;
|
|
18
|
+
};
|
|
19
|
+
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
21
|
+
import { useEffect, useState } from "react";
|
|
22
|
+
import { Loading, useRecordContext } from "react-admin";
|
|
23
|
+
import { useSystemRequestsCache } from "../../contexts/SystemRequestsCache";
|
|
24
|
+
import { FormTabsRenderer, SettingsSections } from "./SettingsSections";
|
|
25
|
+
const AI_PROVIDERS_TAB = "ai";
|
|
26
|
+
const TenancySettingsEdit = ({ method, tenancy }) => {
|
|
27
|
+
const [settingFormatsSchema, setSettingFormatsSchema] = useState(null);
|
|
28
|
+
const { formats: formatsData } = useSystemRequestsCache();
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
var _a;
|
|
31
|
+
if ((_a = formatsData == null ? void 0 : formatsData.data) == null ? void 0 : _a.setting_formats) {
|
|
32
|
+
const parsedSchema = formatsData.data.setting_formats.filter((entry) => entry.tab === AI_PROVIDERS_TAB && entry.visible !== false).map((entry) => {
|
|
33
|
+
var _a2, _b;
|
|
34
|
+
const settingName = entry.attribute ? entry.attribute.replace("settings.", "") : entry.id;
|
|
35
|
+
const defaultValue = (_b = (_a2 = tenancy == null ? void 0 : tenancy.settings) == null ? void 0 : _a2[settingName]) != null ? _b : entry == null ? void 0 : entry.default_value;
|
|
36
|
+
return __spreadValues(__spreadValues({}, entry), defaultValue !== null && defaultValue !== void 0 && {
|
|
37
|
+
fieldOptions: { defaultValue, fullWidth: true }
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
setSettingFormatsSchema(parsedSchema);
|
|
41
|
+
}
|
|
42
|
+
}, [formatsData, tenancy]);
|
|
43
|
+
if (!settingFormatsSchema) {
|
|
44
|
+
return /* @__PURE__ */ jsx(Loading, {});
|
|
45
|
+
}
|
|
46
|
+
return /* @__PURE__ */ jsx(SettingsSections, { schema: settingFormatsSchema, method, translationNamespace: "tenancy.settings" });
|
|
47
|
+
};
|
|
48
|
+
const TenancySettingsView = ({ method, tenancy }) => {
|
|
49
|
+
const [settingFormatsSchema, setSettingFormatsSchema] = useState(null);
|
|
50
|
+
const { formats: formatsData } = useSystemRequestsCache();
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
var _a;
|
|
53
|
+
if ((_a = formatsData == null ? void 0 : formatsData.data) == null ? void 0 : _a.setting_formats) {
|
|
54
|
+
const parsedSchema = formatsData.data.setting_formats.filter((entry) => entry.tab === AI_PROVIDERS_TAB && entry.visible !== false).map((entry) => __spreadProps(__spreadValues({}, entry), { readOnly: true }));
|
|
55
|
+
setSettingFormatsSchema(parsedSchema);
|
|
56
|
+
}
|
|
57
|
+
}, [formatsData]);
|
|
58
|
+
if (!settingFormatsSchema) {
|
|
59
|
+
return /* @__PURE__ */ jsx(Loading, {});
|
|
60
|
+
}
|
|
61
|
+
return /* @__PURE__ */ jsx(SettingsSections, { schema: settingFormatsSchema, method, translationNamespace: "tenancy.settings" });
|
|
62
|
+
};
|
|
63
|
+
const TenancySettingsCreate = ({ method }) => {
|
|
64
|
+
const [settingFormatsSchema, setSettingFormatsSchema] = useState(null);
|
|
65
|
+
const { formats: formatsData, loading } = useSystemRequestsCache();
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
var _a;
|
|
68
|
+
if (!loading && ((_a = formatsData == null ? void 0 : formatsData.data) == null ? void 0 : _a.setting_formats)) {
|
|
69
|
+
setSettingFormatsSchema(
|
|
70
|
+
formatsData.data.setting_formats.filter(
|
|
71
|
+
(entry) => entry.tab === AI_PROVIDERS_TAB && entry.visible !== false
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}, [formatsData, loading]);
|
|
76
|
+
if (!settingFormatsSchema) {
|
|
77
|
+
return /* @__PURE__ */ jsx(Loading, {});
|
|
78
|
+
}
|
|
79
|
+
return /* @__PURE__ */ jsx(FormTabsRenderer, { schema: settingFormatsSchema, method, label: "AI Providers" });
|
|
80
|
+
};
|
|
81
|
+
const TenancySettings = ({ method, attribute, resourceConfig }) => {
|
|
82
|
+
const tenancy = useRecordContext();
|
|
83
|
+
switch (method) {
|
|
84
|
+
case "edit":
|
|
85
|
+
return /* @__PURE__ */ jsx(TenancySettingsEdit, { attribute, method, tenancy, resourceConfig });
|
|
86
|
+
case "view":
|
|
87
|
+
return /* @__PURE__ */ jsx(TenancySettingsView, { attribute, method, tenancy, resourceConfig });
|
|
88
|
+
case "create":
|
|
89
|
+
return /* @__PURE__ */ jsx(TenancySettingsCreate, { attribute, method, resourceConfig });
|
|
90
|
+
case "list":
|
|
91
|
+
default:
|
|
92
|
+
return /* @__PURE__ */ jsx(Fragment, {});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var TenancySettings_default = TenancySettings;
|
|
96
|
+
export {
|
|
97
|
+
TenancySettings_default as default
|
|
98
|
+
};
|
|
@@ -14,82 +14,13 @@ var __spreadValues = (a, b) => {
|
|
|
14
14
|
}
|
|
15
15
|
return a;
|
|
16
16
|
};
|
|
17
|
-
import { Fragment, jsx
|
|
18
|
-
import { DashAutoFormTabs } from "dash-auto-admin";
|
|
17
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
19
18
|
import { useState, useEffect } from "react";
|
|
20
|
-
import { Loading, useRecordContext
|
|
21
|
-
import {
|
|
22
|
-
Box,
|
|
23
|
-
Card,
|
|
24
|
-
CardActionArea,
|
|
25
|
-
CardContent,
|
|
26
|
-
Divider,
|
|
27
|
-
Drawer,
|
|
28
|
-
IconButton,
|
|
29
|
-
Typography
|
|
30
|
-
} from "@mui/material";
|
|
31
|
-
import CloseIcon from "@mui/icons-material/Close";
|
|
19
|
+
import { Loading, useRecordContext } from "react-admin";
|
|
32
20
|
import MUISimpleJsonTable from "../misc/MuiSimpleJsonTable";
|
|
33
21
|
import { useTenantSettingsFormats } from "./TenantSettingsContext";
|
|
34
22
|
import { useSystemRequestsCache } from "../../contexts/SystemRequestsCache";
|
|
35
|
-
|
|
36
|
-
const toSettingsSections = (schema) => {
|
|
37
|
-
const order = [];
|
|
38
|
-
const byGroup = {};
|
|
39
|
-
schema.forEach((entry) => {
|
|
40
|
-
const key = entry.group || entry.tab || "general";
|
|
41
|
-
if (!byGroup[key]) {
|
|
42
|
-
byGroup[key] = [];
|
|
43
|
-
order.push(key);
|
|
44
|
-
}
|
|
45
|
-
byGroup[key].push(entry);
|
|
46
|
-
});
|
|
47
|
-
return order.map((key) => ({ key, entries: byGroup[key] }));
|
|
48
|
-
};
|
|
49
|
-
const prettifyGroup = (key) => key.replace(/[_-]+/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
50
|
-
const SettingsSections = ({ schema, method }) => {
|
|
51
|
-
const translate = useTranslate();
|
|
52
|
-
const [openSection, setOpenSection] = useState(null);
|
|
53
|
-
const sections = toSettingsSections(schema);
|
|
54
|
-
const labelFor = (key) => translate(`tenant.settings.groups.${key}`, { _: prettifyGroup(key) });
|
|
55
|
-
return /* @__PURE__ */ jsxs("section", { children: [
|
|
56
|
-
/* @__PURE__ */ jsx(
|
|
57
|
-
Box,
|
|
58
|
-
{
|
|
59
|
-
sx: {
|
|
60
|
-
display: "grid",
|
|
61
|
-
gap: 2,
|
|
62
|
-
gridTemplateColumns: { xs: "1fr", sm: "repeat(2, 1fr)", md: "repeat(3, 1fr)" }
|
|
63
|
-
},
|
|
64
|
-
children: sections.map((section) => /* @__PURE__ */ jsx(Card, { variant: "outlined", children: /* @__PURE__ */ jsx(CardActionArea, { onClick: () => setOpenSection(section.key), children: /* @__PURE__ */ jsxs(CardContent, { children: [
|
|
65
|
-
/* @__PURE__ */ jsx(Typography, { variant: "subtitle1", sx: { fontWeight: 600 }, children: labelFor(section.key) }),
|
|
66
|
-
/* @__PURE__ */ jsx(Typography, { variant: "caption", color: "text.secondary", children: translate("tenant.settings.option_count", {
|
|
67
|
-
smart_count: section.entries.length,
|
|
68
|
-
_: `${section.entries.length}`
|
|
69
|
-
}) })
|
|
70
|
-
] }) }) }, section.key))
|
|
71
|
-
}
|
|
72
|
-
),
|
|
73
|
-
sections.map((section) => /* @__PURE__ */ jsx(
|
|
74
|
-
Drawer,
|
|
75
|
-
{
|
|
76
|
-
anchor: "right",
|
|
77
|
-
open: openSection === section.key,
|
|
78
|
-
onClose: () => setOpenSection(null),
|
|
79
|
-
ModalProps: { keepMounted: true },
|
|
80
|
-
children: /* @__PURE__ */ jsxs(Box, { sx: { width: { xs: "100vw", sm: 560 }, p: 2 }, children: [
|
|
81
|
-
/* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between", mb: 1 }, children: [
|
|
82
|
-
/* @__PURE__ */ jsx(Typography, { variant: "h6", children: labelFor(section.key) }),
|
|
83
|
-
/* @__PURE__ */ jsx(IconButton, { onClick: () => setOpenSection(null), size: "small", "aria-label": "Close", children: /* @__PURE__ */ jsx(CloseIcon, {}) })
|
|
84
|
-
] }),
|
|
85
|
-
/* @__PURE__ */ jsx(Divider, { sx: { mb: 2 } }),
|
|
86
|
-
/* @__PURE__ */ jsx(FormTabsRenderer, { schema: section.entries, method, label: labelFor(section.key) })
|
|
87
|
-
] })
|
|
88
|
-
},
|
|
89
|
-
section.key
|
|
90
|
-
))
|
|
91
|
-
] });
|
|
92
|
-
};
|
|
23
|
+
import { FormTabsRenderer, SettingsSections } from "./SettingsSections";
|
|
93
24
|
const TenantSettingsEdit = ({ method, attribute, tenant }) => {
|
|
94
25
|
const [settingFormatsSchema, setSettingFormatsSchema] = useState(null);
|
|
95
26
|
const { formats: formatsData, loading } = useSystemRequestsCache();
|
package/dist/systemResources.js
CHANGED
|
@@ -22,6 +22,7 @@ import { SelectInput, FunctionField } from "react-admin";
|
|
|
22
22
|
import ResourceTemplate from "./templates/ResourceTemplate";
|
|
23
23
|
import { TenantImpersonateResource } from "./resources/Tenant/ImpersonateTenantResource";
|
|
24
24
|
import tenantSystemAdminSchema from "./schemas/tenant_superadmin";
|
|
25
|
+
import TenancySettings from "./components/tenant/TenancySettings";
|
|
25
26
|
import subscriptionPlanSchema from "./schemas/subscriptionPlan";
|
|
26
27
|
import { RutValidator } from "./utils/validators";
|
|
27
28
|
import { DASHAppConstants } from "dash-constants";
|
|
@@ -55,6 +56,21 @@ const systemResources = [
|
|
|
55
56
|
roles: [DASHAppConstants.system.SYSTEM_ROLE],
|
|
56
57
|
trash: true,
|
|
57
58
|
component: ResourceTemplate,
|
|
59
|
+
// Same F21 dynamic-tab pattern the sibling system/tenant resource
|
|
60
|
+
// already uses below — fetches config('tenancies.setting_formats')
|
|
61
|
+
// (core + whatever domain merges onto it, e.g. the AI Providers
|
|
62
|
+
// governance tab) once per edit/view, cached client-side.
|
|
63
|
+
contextComponent: ({ resourceConfig, mode, children }) => {
|
|
64
|
+
return mode === "list" ? children : /* @__PURE__ */ jsx(
|
|
65
|
+
SystemRequestsCache,
|
|
66
|
+
{
|
|
67
|
+
cacheKey: "tenancy_formats_cache",
|
|
68
|
+
apiUrl: "system/tenancy/systemSettingFormats",
|
|
69
|
+
cacheSeconds: 300,
|
|
70
|
+
children
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
},
|
|
58
74
|
model: "system/tenancy",
|
|
59
75
|
//group: "resource.groups.system_resources",
|
|
60
76
|
group: "Accounts",
|
|
@@ -332,6 +348,18 @@ const systemResources = [
|
|
|
332
348
|
inEdit: false,
|
|
333
349
|
inShow: true,
|
|
334
350
|
inCreate: false
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
tab: "AI Providers",
|
|
354
|
+
label: "AI Providers",
|
|
355
|
+
attribute: "settings",
|
|
356
|
+
// custom component owns rendering; 'attribute' is required by the interface, not used directly
|
|
357
|
+
type: Object,
|
|
358
|
+
custom: true,
|
|
359
|
+
inList: false,
|
|
360
|
+
component: TenancySettings,
|
|
361
|
+
inCreate: false,
|
|
362
|
+
inShow: false
|
|
335
363
|
}
|
|
336
364
|
],
|
|
337
365
|
label: "Accounts",
|
|
@@ -372,7 +400,27 @@ const systemResources = [
|
|
|
372
400
|
}
|
|
373
401
|
],
|
|
374
402
|
mutationMode: "pessimistic",
|
|
375
|
-
redirectAfterUpdate: false
|
|
403
|
+
redirectAfterUpdate: false,
|
|
404
|
+
// Defensive normalization, same pattern already proven in
|
|
405
|
+
// kt-ecommerce's tenant/tenant resource's own postFormatter: if
|
|
406
|
+
// 'settings' ever ends up as a JS Array (react-hook-form keeps it
|
|
407
|
+
// array-shaped once seeded that way, even after nested dot-path
|
|
408
|
+
// fields like 'settings.bionic_mode' are set on it), JSON.stringify
|
|
409
|
+
// silently drops the non-index keys on submit — the AI Providers tab
|
|
410
|
+
// then looks saved in the UI but nothing persists. The backend now
|
|
411
|
+
// always seeds 'settings' as an object on load (SystemTenancyResource
|
|
412
|
+
// + TenancyController::_postGetOne), which should prevent this at
|
|
413
|
+
// the source, but this stays as a second line of defense — cheap,
|
|
414
|
+
// and it's exactly what already shipped for the sibling resource.
|
|
415
|
+
postFormatter: (params) => {
|
|
416
|
+
if (Array.isArray(params.settings)) {
|
|
417
|
+
params.settings = Object.fromEntries(Object.entries(params.settings));
|
|
418
|
+
}
|
|
419
|
+
if (Array.isArray(params.metadata)) {
|
|
420
|
+
params.metadata = Object.fromEntries(Object.entries(params.metadata));
|
|
421
|
+
}
|
|
422
|
+
return params;
|
|
423
|
+
}
|
|
376
424
|
}, drawerSettings),
|
|
377
425
|
__spreadProps(__spreadValues({
|
|
378
426
|
roles: [DASHAppConstants.system.SYSTEM_ROLE],
|
|
@@ -437,6 +485,12 @@ const systemResources = [
|
|
|
437
485
|
search: false,
|
|
438
486
|
exporter: false,
|
|
439
487
|
postFormatter: (params) => {
|
|
488
|
+
if (Array.isArray(params.settings)) {
|
|
489
|
+
params.settings = Object.fromEntries(Object.entries(params.settings));
|
|
490
|
+
}
|
|
491
|
+
if (Array.isArray(params.attributes)) {
|
|
492
|
+
params.attributes = Object.fromEntries(Object.entries(params.attributes));
|
|
493
|
+
}
|
|
440
494
|
if (params.systemMarketplaces) {
|
|
441
495
|
params.system_marketplace_ids = params.systemMarketplaces.map((item) => item.id);
|
|
442
496
|
}
|
|
@@ -456,6 +510,167 @@ const systemResources = [
|
|
|
456
510
|
//toolbarCreateButton: { enabled: true },
|
|
457
511
|
//listEditButton: { enabled: true, component: QuickEditButton, props: { icon: <Bolt />, label: "", resource: "tenant/inline", navigation:"virtualhash", navigate: (id) => id, size: "small", color: "secondary" }},
|
|
458
512
|
}),
|
|
513
|
+
{
|
|
514
|
+
// Platform-wide AI provider credentials (bionic/ollama/deepinfra/
|
|
515
|
+
// aws_bedrock) — see backend App\AiAgentCore\Models\SystemAiProviderKey.
|
|
516
|
+
// The 4 rows are seeded by migration and never created/deleted here;
|
|
517
|
+
// an admin only ever edits an existing row's credentials/flags.
|
|
518
|
+
// Credential VALUES are never returned by the API once saved
|
|
519
|
+
// (SystemAiProviderKeyResource only exposes has_api_key/
|
|
520
|
+
// has_access_key_id/has_secret_access_key) — the fields below are
|
|
521
|
+
// write-only by design, same convention as any masked secret field.
|
|
522
|
+
roles: [DASHAppConstants.system.SYSTEM_ROLE],
|
|
523
|
+
component: ResourceTemplate,
|
|
524
|
+
model: "system/system_ai_provider_key",
|
|
525
|
+
group: "Accounts",
|
|
526
|
+
label: "AI Providers",
|
|
527
|
+
icon: /* @__PURE__ */ jsx(SystemUpdateAlt, {}),
|
|
528
|
+
create: false,
|
|
529
|
+
schema: [
|
|
530
|
+
{
|
|
531
|
+
tab: "General",
|
|
532
|
+
label: "Provider",
|
|
533
|
+
attribute: "provider",
|
|
534
|
+
type: String,
|
|
535
|
+
inList: true,
|
|
536
|
+
inEdit: false,
|
|
537
|
+
inShow: true,
|
|
538
|
+
inCreate: false
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
tab: "General",
|
|
542
|
+
label: "Active",
|
|
543
|
+
attribute: "is_active",
|
|
544
|
+
type: Boolean,
|
|
545
|
+
inList: true,
|
|
546
|
+
inEdit: true,
|
|
547
|
+
inShow: true,
|
|
548
|
+
inCreate: false
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
tab: "General",
|
|
552
|
+
label: "Platform Default Provider",
|
|
553
|
+
attribute: "is_default",
|
|
554
|
+
type: Boolean,
|
|
555
|
+
inList: true,
|
|
556
|
+
inEdit: true,
|
|
557
|
+
inShow: true,
|
|
558
|
+
inCreate: false,
|
|
559
|
+
fieldProps: {
|
|
560
|
+
helperText: "The fallback provider used platform-wide, and for any Tenancy that disables a provider without setting its own default."
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
tab: "General",
|
|
565
|
+
label: "API Key",
|
|
566
|
+
attribute: "credentials.api_key",
|
|
567
|
+
type: String,
|
|
568
|
+
inList: false,
|
|
569
|
+
inEdit: true,
|
|
570
|
+
inShow: false,
|
|
571
|
+
inCreate: false,
|
|
572
|
+
fieldProps: {
|
|
573
|
+
helperText: "Bionic / Ollama / DeepInfra only. Write-only \u2014 leave blank to keep the current value."
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
tab: "General",
|
|
578
|
+
label: "AWS Access Key ID",
|
|
579
|
+
attribute: "credentials.access_key_id",
|
|
580
|
+
type: String,
|
|
581
|
+
inList: false,
|
|
582
|
+
inEdit: true,
|
|
583
|
+
inShow: false,
|
|
584
|
+
inCreate: false,
|
|
585
|
+
fieldProps: {
|
|
586
|
+
helperText: "AWS Bedrock only. Write-only \u2014 leave blank to keep the current value."
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
tab: "General",
|
|
591
|
+
label: "AWS Secret Access Key",
|
|
592
|
+
attribute: "credentials.secret_access_key",
|
|
593
|
+
type: String,
|
|
594
|
+
inList: false,
|
|
595
|
+
inEdit: true,
|
|
596
|
+
inShow: false,
|
|
597
|
+
inCreate: false,
|
|
598
|
+
fieldProps: {
|
|
599
|
+
helperText: "AWS Bedrock only. Write-only \u2014 leave blank to keep the current value."
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
// The platform-wide floor of SystemAiProviderKey::resolveServiceTier()'s
|
|
604
|
+
// 3-level cascade (System -> Tenancy -> Lab) — see
|
|
605
|
+
// vanexa-backend-domain/docs/CASCADING_AI_SETTINGS_KEY_PERMISSIONS.md.
|
|
606
|
+
// Reaches for the SAME static-choices 'select'/'multiselect' types the
|
|
607
|
+
// Tenancy-level "AI Providers" tab already uses (dash-auto-admin's
|
|
608
|
+
// AttributeToInput/AttributeToField) — a hand-written schema like this
|
|
609
|
+
// one is a normal consumer of those types too, not just the dynamic
|
|
610
|
+
// setting_formats pipeline.
|
|
611
|
+
tab: "General",
|
|
612
|
+
label: "DeepInfra Service Tier (Platform Default)",
|
|
613
|
+
attribute: "settings.service_tier",
|
|
614
|
+
type: "select",
|
|
615
|
+
inList: false,
|
|
616
|
+
inEdit: true,
|
|
617
|
+
inShow: true,
|
|
618
|
+
inCreate: false,
|
|
619
|
+
options: [
|
|
620
|
+
{ id: "flex", name: "Flex (0.8x \u2014 best-effort, may wait or be rejected)" },
|
|
621
|
+
{ id: "standard", name: "Standard (1.0x)" },
|
|
622
|
+
{ id: "priority", name: "Priority (1.5x \u2014 skips queues)" }
|
|
623
|
+
],
|
|
624
|
+
fieldProps: {
|
|
625
|
+
helperText: "DeepInfra only. Used whenever a Tenancy has not set its own default.",
|
|
626
|
+
// Matches resolveServiceTier()'s OWN eventual fallback
|
|
627
|
+
// (SystemAiProviderKey::SERVICE_TIER_DEFAULT) — a fresh
|
|
628
|
+
// row with no settings yet shows what would actually be
|
|
629
|
+
// used, not a misleadingly blank field.
|
|
630
|
+
defaultValue: "standard"
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
tab: "General",
|
|
635
|
+
label: "DeepInfra Tiers Any Tenancy May Ever Use",
|
|
636
|
+
attribute: "settings.service_tier_allowed",
|
|
637
|
+
type: "multiselect",
|
|
638
|
+
inList: false,
|
|
639
|
+
inEdit: true,
|
|
640
|
+
inShow: true,
|
|
641
|
+
inCreate: false,
|
|
642
|
+
options: [
|
|
643
|
+
{ id: "flex", name: "Flex (0.8x \u2014 best-effort, may wait or be rejected)" },
|
|
644
|
+
{ id: "standard", name: "Standard (1.0x)" },
|
|
645
|
+
{ id: "priority", name: "Priority (1.5x \u2014 skips queues)" }
|
|
646
|
+
],
|
|
647
|
+
fieldProps: {
|
|
648
|
+
helperText: "DeepInfra only. The HARD CEILING \u2014 a Tenancy's own allow-list can only narrow this, never exceed it. Leave all three selected unless a tier should be forbidden platform-wide.",
|
|
649
|
+
// Matches resolveServiceTier()'s own default when this
|
|
650
|
+
// key has never been saved (SystemAiProviderKey::
|
|
651
|
+
// providerSetting()'s $default param, called with
|
|
652
|
+
// SERVICE_TIERS) — an unrestricted ceiling, shown as such
|
|
653
|
+
// rather than as an empty, seemingly-forbid-everything list.
|
|
654
|
+
defaultValue: ["flex", "standard", "priority"]
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
],
|
|
658
|
+
menu: [
|
|
659
|
+
{
|
|
660
|
+
title: "AI Providers",
|
|
661
|
+
redirect: "/system/system_ai_provider_key"
|
|
662
|
+
}
|
|
663
|
+
],
|
|
664
|
+
mutationMode: "pessimistic",
|
|
665
|
+
redirectAfterUpdate: false,
|
|
666
|
+
drawer: true,
|
|
667
|
+
drawerOptions: {
|
|
668
|
+
view: false,
|
|
669
|
+
edit: true,
|
|
670
|
+
create: false
|
|
671
|
+
},
|
|
672
|
+
listEditButton: { props: { buttonProps: { variant: "text", size: "small" }, label: "" } }
|
|
673
|
+
},
|
|
459
674
|
__spreadValues({
|
|
460
675
|
roles: [DASHAppConstants.system.SYSTEM_ROLE],
|
|
461
676
|
component: ResourceTemplate,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dashadmin/dash-admin",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.72",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -38,11 +38,11 @@
|
|
|
38
38
|
"@rollup/plugin-node-resolve": "latest",
|
|
39
39
|
"@rollup/plugin-replace": "latest",
|
|
40
40
|
"@rollup/plugin-typescript": "latest",
|
|
41
|
-
"@dashadmin/dash-admin-state": "1.3.
|
|
42
|
-
"@dashadmin/dash-auto-admin": "1.3.
|
|
43
|
-
"@dashadmin/dash-constants": "1.3.
|
|
44
|
-
"@dashadmin/dash-dialog": "1.3.
|
|
45
|
-
"@dashadmin/dash-styles": "1.3.
|
|
41
|
+
"@dashadmin/dash-admin-state": "1.3.72",
|
|
42
|
+
"@dashadmin/dash-auto-admin": "1.3.72",
|
|
43
|
+
"@dashadmin/dash-constants": "1.3.72",
|
|
44
|
+
"@dashadmin/dash-dialog": "1.3.72",
|
|
45
|
+
"@dashadmin/dash-styles": "1.3.72",
|
|
46
46
|
"framer-motion": "latest",
|
|
47
47
|
"prop-types": "latest",
|
|
48
48
|
"query-string": "latest",
|
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
"rollup-plugin-dts": "latest",
|
|
62
62
|
"rollup-plugin-peer-deps-external": "latest",
|
|
63
63
|
"rollup-plugin-svg": "latest",
|
|
64
|
-
"@dashadmin/dash-modal": "1.3.
|
|
65
|
-
"@dashadmin/dash-components": "1.3.
|
|
66
|
-
"@dashadmin/dash-axios-hook": "1.3.
|
|
67
|
-
"@dashadmin/dash-utils": "1.3.
|
|
68
|
-
"@dashadmin/dash-auth": "1.3.
|
|
64
|
+
"@dashadmin/dash-modal": "1.3.72",
|
|
65
|
+
"@dashadmin/dash-components": "1.3.72",
|
|
66
|
+
"@dashadmin/dash-axios-hook": "1.3.72",
|
|
67
|
+
"@dashadmin/dash-utils": "1.3.72",
|
|
68
|
+
"@dashadmin/dash-auth": "1.3.72"
|
|
69
69
|
},
|
|
70
70
|
"devDependencies": {
|
|
71
71
|
"@babel/core": "latest",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
"@types/react-dom": "latest",
|
|
79
79
|
"@zerollup/ts-transform-paths": "latest",
|
|
80
80
|
"babel-loader": "latest",
|
|
81
|
-
"@dashadmin/dash-eslint": "1.3.
|
|
82
|
-
"@dashadmin/dash-tsconfig": "1.3.
|
|
81
|
+
"@dashadmin/dash-eslint": "1.3.72",
|
|
82
|
+
"@dashadmin/dash-tsconfig": "1.3.72",
|
|
83
83
|
"eslint": "latest",
|
|
84
84
|
"eslint-config-airbnb": "latest",
|
|
85
85
|
"eslint-config-airbnb-typescript": "latest",
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"eslint-plugin-prettier": "latest",
|
|
94
94
|
"eslint-plugin-react": "latest",
|
|
95
95
|
"eslint-plugin-react-hooks": "latest",
|
|
96
|
-
"@dashadmin/dash-prettier": "1.3.
|
|
96
|
+
"@dashadmin/dash-prettier": "1.3.72",
|
|
97
97
|
"query-string": "latest",
|
|
98
98
|
"react-dom": "19.2.8",
|
|
99
99
|
"rollup": "latest",
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { DashAutoFormTabs } from "dash-auto-admin";
|
|
2
|
+
import React, { useState } from "react";
|
|
3
|
+
import { useTranslate } from "react-admin";
|
|
4
|
+
import {
|
|
5
|
+
Box,
|
|
6
|
+
Card,
|
|
7
|
+
CardActionArea,
|
|
8
|
+
CardContent,
|
|
9
|
+
Divider,
|
|
10
|
+
Drawer,
|
|
11
|
+
IconButton,
|
|
12
|
+
Typography,
|
|
13
|
+
} from "@mui/material";
|
|
14
|
+
import CloseIcon from "@mui/icons-material/Close";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Shared by TenantSettings.tsx and TenancySettings.tsx — both render the
|
|
18
|
+
* same "one card per settings group, each opening a drawer" layout over a
|
|
19
|
+
* backend-declared setting_formats schema, the only difference being which
|
|
20
|
+
* record (Tenant vs Tenancy) supplies the current values. Extracted here
|
|
21
|
+
* rather than duplicated a second time WITHIN this package (unlike
|
|
22
|
+
* kt-ecommerce's own TenantSettings.tsx twin, which stays separate on
|
|
23
|
+
* purpose — see that file's docblock — because pulling a cross-PACKAGE
|
|
24
|
+
* shared export would break the *-system apps until dash-admin is
|
|
25
|
+
* republished; that constraint doesn't apply to two components already in
|
|
26
|
+
* the same package).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Wrapper creating a React component boundary so DashAutoFormTabs hooks don't violate Rules of Hooks */
|
|
30
|
+
export const FormTabsRenderer: React.FC<{ schema: any; method: "list" | "create" | "edit" | "view"; label: string }> = ({ schema, method, label }) => (
|
|
31
|
+
<>{DashAutoFormTabs({ schema, resourceConfig: null, options: { mode: method, label } })}</>
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Split the settings schema into the cards shown on the configuration tab.
|
|
36
|
+
*
|
|
37
|
+
* Grouped by the `group` each setting format already declares — data-driven, so
|
|
38
|
+
* this is agnostic to which product is running: a domain that ships different
|
|
39
|
+
* groups gets different cards with no change here, and one that ships none at
|
|
40
|
+
* all still gets a single card rather than an error.
|
|
41
|
+
*
|
|
42
|
+
* `group` rather than `tab`: tab is the coarser of the two, and a domain that
|
|
43
|
+
* puts everything on one tab (the common case) would get no split at all.
|
|
44
|
+
*/
|
|
45
|
+
const toSettingsSections = (schema: any[]) => {
|
|
46
|
+
const order: string[] = [];
|
|
47
|
+
const byGroup: Record<string, any[]> = {};
|
|
48
|
+
|
|
49
|
+
schema.forEach((entry) => {
|
|
50
|
+
const key = entry.group || entry.tab || 'general';
|
|
51
|
+
|
|
52
|
+
if (!byGroup[key]) {
|
|
53
|
+
byGroup[key] = [];
|
|
54
|
+
order.push(key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
byGroup[key].push(entry);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return order.map((key) => ({ key, entries: byGroup[key] }));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const prettifyGroup = (key: string) =>
|
|
64
|
+
key.replace(/[_-]+/g, ' ').replace(/^./, (c) => c.toUpperCase());
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The configuration panel: one card per settings group, each opening a drawer.
|
|
68
|
+
*
|
|
69
|
+
* Replaces a single flat list that grew to whatever the backend declared —
|
|
70
|
+
* unreadable once a product ships more than a handful of settings.
|
|
71
|
+
*
|
|
72
|
+
* ── Two things this has to get right ─────────────────────────────────────────
|
|
73
|
+
*
|
|
74
|
+
* keepMounted: the fields belong to the surrounding react-hook-form. Unmounting
|
|
75
|
+
* a closed drawer unregisters them and drops edits made in a section the user
|
|
76
|
+
* closed before saving. Context reaches through the portal, so the form itself
|
|
77
|
+
* is fine — it is only unmount that loses data.
|
|
78
|
+
*
|
|
79
|
+
* z-index: this panel is often rendered INSIDE a Drawer already (the tenancy/
|
|
80
|
+
* tenant resource opens records in one). Two Drawers both sit at
|
|
81
|
+
* theme.zIndex.modal, where stacking falls to DOM order; raising this one
|
|
82
|
+
* makes it deliberate.
|
|
83
|
+
*
|
|
84
|
+
* Labels resolve through the translator, so a domain can name its own groups.
|
|
85
|
+
* A missing key falls back to the prettified group key rather than rendering a
|
|
86
|
+
* raw translation string.
|
|
87
|
+
*/
|
|
88
|
+
export const SettingsSections: React.FC<{
|
|
89
|
+
schema: any[];
|
|
90
|
+
method: "list" | "create" | "edit" | "view";
|
|
91
|
+
/** Translation namespace prefix — 'tenant.settings' or 'tenancy.settings'. */
|
|
92
|
+
translationNamespace?: string;
|
|
93
|
+
}> = ({ schema, method, translationNamespace = 'tenant.settings' }) => {
|
|
94
|
+
const translate = useTranslate();
|
|
95
|
+
const [openSection, setOpenSection] = useState<string | null>(null);
|
|
96
|
+
const sections = toSettingsSections(schema);
|
|
97
|
+
|
|
98
|
+
const labelFor = (key: string) =>
|
|
99
|
+
translate(`${translationNamespace}.groups.${key}`, { _: prettifyGroup(key) });
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<section>
|
|
103
|
+
<Box
|
|
104
|
+
sx={{
|
|
105
|
+
display: 'grid',
|
|
106
|
+
gap: 2,
|
|
107
|
+
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{sections.map((section) => (
|
|
111
|
+
<Card key={section.key} variant="outlined">
|
|
112
|
+
<CardActionArea onClick={() => setOpenSection(section.key)}>
|
|
113
|
+
<CardContent>
|
|
114
|
+
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
|
115
|
+
{labelFor(section.key)}
|
|
116
|
+
</Typography>
|
|
117
|
+
<Typography variant="caption" color="text.secondary">
|
|
118
|
+
{translate(`${translationNamespace}.option_count`, {
|
|
119
|
+
smart_count: section.entries.length,
|
|
120
|
+
_: `${section.entries.length}`,
|
|
121
|
+
})}
|
|
122
|
+
</Typography>
|
|
123
|
+
</CardContent>
|
|
124
|
+
</CardActionArea>
|
|
125
|
+
</Card>
|
|
126
|
+
))}
|
|
127
|
+
</Box>
|
|
128
|
+
|
|
129
|
+
{sections.map((section) => (
|
|
130
|
+
<Drawer
|
|
131
|
+
key={section.key}
|
|
132
|
+
anchor="right"
|
|
133
|
+
open={openSection === section.key}
|
|
134
|
+
onClose={() => setOpenSection(null)}
|
|
135
|
+
ModalProps={{ keepMounted: true }}
|
|
136
|
+
>
|
|
137
|
+
<Box sx={{ width: { xs: '100vw', sm: 560 }, p: 2 }}>
|
|
138
|
+
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
|
139
|
+
<Typography variant="h6">{labelFor(section.key)}</Typography>
|
|
140
|
+
<IconButton onClick={() => setOpenSection(null)} size="small" aria-label="Close">
|
|
141
|
+
<CloseIcon />
|
|
142
|
+
</IconButton>
|
|
143
|
+
</Box>
|
|
144
|
+
<Divider sx={{ mb: 2 }} />
|
|
145
|
+
<FormTabsRenderer schema={section.entries} method={method} label={labelFor(section.key)} />
|
|
146
|
+
</Box>
|
|
147
|
+
</Drawer>
|
|
148
|
+
))}
|
|
149
|
+
</section>
|
|
150
|
+
);
|
|
151
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { IDashAutoAdminCustomFieldComponent } from "dash-auto-admin";
|
|
2
|
+
import React, { useEffect, useState } from "react";
|
|
3
|
+
import { Loading, useRecordContext } from "react-admin";
|
|
4
|
+
|
|
5
|
+
import { useSystemRequestsCache } from "../../contexts/SystemRequestsCache";
|
|
6
|
+
import { FormTabsRenderer, SettingsSections } from "./SettingsSections";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The Tenancy-level twin of TenantSettings.tsx — same backend-driven
|
|
10
|
+
* card+drawer settings layout (shared via ./SettingsSections.tsx), reading
|
|
11
|
+
* a Tenancy record's `settings` JSON column instead of a Tenant's. Feeds
|
|
12
|
+
* the system app's Tenancy edit screen "AI Providers" tab (per-provider
|
|
13
|
+
* system_key/tenant_key/disabled mode + the tenancy's own default-provider
|
|
14
|
+
* fallback — see the backend's config/ai_tenancy_settings.php) with no
|
|
15
|
+
* further frontend change if a domain adds more tenancy-level settings
|
|
16
|
+
* later, the same guarantee TenantSettings.tsx already makes for Tenant.
|
|
17
|
+
*
|
|
18
|
+
* Requires the resource's contextComponent to wrap edit/view in
|
|
19
|
+
* <SystemRequestsCache apiUrl="system/tenancy/systemSettingFormats" ...>
|
|
20
|
+
* — see systemResources.tsx's `system/tenancy` resource.
|
|
21
|
+
*
|
|
22
|
+
* FILTERED TO tab === 'ai' — `system/tenancy/systemSettingFormats` returns
|
|
23
|
+
* EVERY tenancy setting_formats entry, including core's pre-existing
|
|
24
|
+
* billing-tab fields (billing_email/tax_id_type/auto_pay_enabled,
|
|
25
|
+
* config/tenancies.php) that have never had a dedicated UI before this
|
|
26
|
+
* component's own contextComponent wiring was added. Mirrors
|
|
27
|
+
* TenantTheme.tsx's own `.filter((entry) => entry.tab === 'colors')` — this
|
|
28
|
+
* component owns exactly ONE tab, not "everything the endpoint returns".
|
|
29
|
+
*/
|
|
30
|
+
const AI_PROVIDERS_TAB = 'ai';
|
|
31
|
+
|
|
32
|
+
const TenancySettingsEdit: React.FC<IDashAutoAdminCustomFieldComponent & { tenancy: any }> = ({ method, tenancy }) => {
|
|
33
|
+
const [settingFormatsSchema, setSettingFormatsSchema] = useState<any>(null);
|
|
34
|
+
const { formats: formatsData } = useSystemRequestsCache();
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (formatsData?.data?.setting_formats) {
|
|
38
|
+
const parsedSchema = formatsData.data.setting_formats
|
|
39
|
+
.filter((entry: any) => entry.tab === AI_PROVIDERS_TAB && entry.visible !== false)
|
|
40
|
+
.map((entry: any) => {
|
|
41
|
+
// 'settings.foo' -> 'foo', the actual key on the record.
|
|
42
|
+
const settingName = entry.attribute ? entry.attribute.replace('settings.', '') : entry.id;
|
|
43
|
+
const defaultValue = tenancy?.settings?.[settingName] ?? entry?.default_value;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
...entry,
|
|
47
|
+
...(defaultValue !== null && defaultValue !== undefined && {
|
|
48
|
+
fieldOptions: { defaultValue, fullWidth: true },
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
setSettingFormatsSchema(parsedSchema);
|
|
54
|
+
}
|
|
55
|
+
}, [formatsData, tenancy]);
|
|
56
|
+
|
|
57
|
+
if (!settingFormatsSchema) {
|
|
58
|
+
return <Loading />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return <SettingsSections schema={settingFormatsSchema} method={method} translationNamespace="tenancy.settings" />;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const TenancySettingsView: React.FC<IDashAutoAdminCustomFieldComponent & { tenancy: any }> = ({ method, tenancy }) => {
|
|
65
|
+
const [settingFormatsSchema, setSettingFormatsSchema] = useState<any>(null);
|
|
66
|
+
const { formats: formatsData } = useSystemRequestsCache();
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (formatsData?.data?.setting_formats) {
|
|
70
|
+
const parsedSchema = formatsData.data.setting_formats
|
|
71
|
+
.filter((entry: any) => entry.tab === AI_PROVIDERS_TAB && entry.visible !== false)
|
|
72
|
+
.map((entry: any) => ({ ...entry, readOnly: true }));
|
|
73
|
+
|
|
74
|
+
setSettingFormatsSchema(parsedSchema);
|
|
75
|
+
}
|
|
76
|
+
}, [formatsData]);
|
|
77
|
+
|
|
78
|
+
if (!settingFormatsSchema) {
|
|
79
|
+
return <Loading />;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return <SettingsSections schema={settingFormatsSchema} method={method} translationNamespace="tenancy.settings" />;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const TenancySettingsCreate: React.FC<IDashAutoAdminCustomFieldComponent> = ({ method }) => {
|
|
86
|
+
const [settingFormatsSchema, setSettingFormatsSchema] = useState<any>(null);
|
|
87
|
+
const { formats: formatsData, loading } = useSystemRequestsCache();
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!loading && formatsData?.data?.setting_formats) {
|
|
91
|
+
setSettingFormatsSchema(
|
|
92
|
+
formatsData.data.setting_formats.filter(
|
|
93
|
+
(entry: any) => entry.tab === AI_PROVIDERS_TAB && entry.visible !== false,
|
|
94
|
+
),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}, [formatsData, loading]);
|
|
98
|
+
|
|
99
|
+
if (!settingFormatsSchema) {
|
|
100
|
+
return <Loading />;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return <FormTabsRenderer schema={settingFormatsSchema} method={method} label="AI Providers" />;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const TenancySettings = ({ method, attribute, resourceConfig }: IDashAutoAdminCustomFieldComponent) => {
|
|
107
|
+
const tenancy = useRecordContext();
|
|
108
|
+
|
|
109
|
+
switch (method) {
|
|
110
|
+
case "edit":
|
|
111
|
+
return <TenancySettingsEdit attribute={attribute} method={method} tenancy={tenancy} resourceConfig={resourceConfig} />;
|
|
112
|
+
case "view":
|
|
113
|
+
return <TenancySettingsView attribute={attribute} method={method} tenancy={tenancy} resourceConfig={resourceConfig} />;
|
|
114
|
+
case "create":
|
|
115
|
+
return <TenancySettingsCreate attribute={attribute} method={method} resourceConfig={resourceConfig} />;
|
|
116
|
+
case "list":
|
|
117
|
+
default:
|
|
118
|
+
return <></>;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export default TenancySettings;
|
|
@@ -1,32 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import React, { useState, useEffect
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
Box,
|
|
6
|
-
Card,
|
|
7
|
-
CardActionArea,
|
|
8
|
-
CardContent,
|
|
9
|
-
Divider,
|
|
10
|
-
Drawer,
|
|
11
|
-
IconButton,
|
|
12
|
-
Typography,
|
|
13
|
-
} from "@mui/material";
|
|
14
|
-
import CloseIcon from "@mui/icons-material/Close";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import { useAxios } from 'dash-axios-hook';
|
|
19
|
-
import { useFormContext, useWatch } from "react-hook-form";
|
|
1
|
+
import { IDashAutoAdminCustomFieldComponent } from "dash-auto-admin";
|
|
2
|
+
import React, { useState, useEffect } from "react";
|
|
3
|
+
import { Loading, useRecordContext } from "react-admin";
|
|
4
|
+
|
|
20
5
|
import { Tenant } from "../../interfaces/Tenant";
|
|
21
6
|
import MUISimpleJsonTable from "../misc/MuiSimpleJsonTable";
|
|
22
7
|
import { useTenantSettingsFormats } from "./TenantSettingsContext";
|
|
23
8
|
import { useSystemRequestsCache } from "../../contexts/SystemRequestsCache";
|
|
24
|
-
|
|
25
|
-
/** Wrapper creating a React component boundary so DashAutoFormTabs hooks don't violate Rules of Hooks */
|
|
26
|
-
const FormTabsRenderer: React.FC<{schema: any; method: "list" | "create" | "edit" | "view"; label: string}> = ({schema, method, label}) => (
|
|
27
|
-
<>{DashAutoFormTabs({schema, resourceConfig: null, options: {mode: method, label}})}</>
|
|
28
|
-
);
|
|
29
|
-
|
|
9
|
+
import { FormTabsRenderer, SettingsSections } from "./SettingsSections";
|
|
30
10
|
|
|
31
11
|
/**
|
|
32
12
|
* NOTE — there is a second, behaviourally identical implementation of this
|
|
@@ -39,122 +19,11 @@ const FormTabsRenderer: React.FC<{schema: any; method: "list" | "create" | "edit
|
|
|
39
19
|
* anyone without LINK_DASH_CORE=true — so it would break the *-system apps
|
|
40
20
|
* until dash-admin is republished. Merge them in the same change that ships a
|
|
41
21
|
* new dash-admin version, not before.
|
|
42
|
-
*/
|
|
43
|
-
/**
|
|
44
|
-
* Split the settings schema into the cards shown on the configuration tab.
|
|
45
|
-
*
|
|
46
|
-
* Grouped by the `group` each setting format already declares — data-driven, so
|
|
47
|
-
* this is agnostic to which product is running: a domain that ships different
|
|
48
|
-
* groups gets different cards with no change here, and one that ships none at
|
|
49
|
-
* all still gets a single card rather than an error.
|
|
50
|
-
*
|
|
51
|
-
* `group` rather than `tab`: tab is the coarser of the two, and a domain that
|
|
52
|
-
* puts everything on one tab (the common case) would get no split at all.
|
|
53
|
-
*/
|
|
54
|
-
const toSettingsSections = (schema: any[]) => {
|
|
55
|
-
const order: string[] = [];
|
|
56
|
-
const byGroup: Record<string, any[]> = {};
|
|
57
|
-
|
|
58
|
-
schema.forEach((entry) => {
|
|
59
|
-
const key = entry.group || entry.tab || 'general';
|
|
60
|
-
|
|
61
|
-
if (!byGroup[key]) {
|
|
62
|
-
byGroup[key] = [];
|
|
63
|
-
order.push(key);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
byGroup[key].push(entry);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
return order.map((key) => ({ key, entries: byGroup[key] }));
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const prettifyGroup = (key: string) =>
|
|
73
|
-
key.replace(/[_-]+/g, ' ').replace(/^./, (c) => c.toUpperCase());
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* The configuration panel: one card per settings group, each opening a drawer.
|
|
77
|
-
*
|
|
78
|
-
* Replaces a single flat list that grew to whatever the backend declared —
|
|
79
|
-
* unreadable once a product ships more than a handful of settings.
|
|
80
|
-
*
|
|
81
|
-
* ── Two things this has to get right ─────────────────────────────────────────
|
|
82
|
-
*
|
|
83
|
-
* keepMounted: the fields belong to the surrounding react-hook-form. Unmounting
|
|
84
|
-
* a closed drawer unregisters them and drops edits made in a section the user
|
|
85
|
-
* closed before saving. Context reaches through the portal, so the form itself
|
|
86
|
-
* is fine — it is only unmount that loses data.
|
|
87
|
-
*
|
|
88
|
-
* z-index: this panel is often rendered INSIDE a Drawer already (the tenancy
|
|
89
|
-
* resource opens records in one). Two Drawers both sit at theme.zIndex.modal,
|
|
90
|
-
* where stacking falls to DOM order; raising this one makes it deliberate.
|
|
91
22
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
23
|
+
* The card+drawer layout itself lives in ./SettingsSections.tsx, shared with
|
|
24
|
+
* TenancySettings.tsx — that extraction is safe because both are already in
|
|
25
|
+
* THIS package, unlike the kt-ecommerce case above.
|
|
95
26
|
*/
|
|
96
|
-
const SettingsSections: React.FC<{
|
|
97
|
-
schema: any[];
|
|
98
|
-
method: "list" | "create" | "edit" | "view";
|
|
99
|
-
}> = ({ schema, method }) => {
|
|
100
|
-
const translate = useTranslate();
|
|
101
|
-
const [openSection, setOpenSection] = useState<string | null>(null);
|
|
102
|
-
const sections = toSettingsSections(schema);
|
|
103
|
-
|
|
104
|
-
const labelFor = (key: string) =>
|
|
105
|
-
translate(`tenant.settings.groups.${key}`, { _: prettifyGroup(key) });
|
|
106
|
-
|
|
107
|
-
return (
|
|
108
|
-
<section>
|
|
109
|
-
<Box
|
|
110
|
-
sx={{
|
|
111
|
-
display: 'grid',
|
|
112
|
-
gap: 2,
|
|
113
|
-
gridTemplateColumns: { xs: '1fr', sm: 'repeat(2, 1fr)', md: 'repeat(3, 1fr)' },
|
|
114
|
-
}}
|
|
115
|
-
>
|
|
116
|
-
{sections.map((section) => (
|
|
117
|
-
<Card key={section.key} variant="outlined">
|
|
118
|
-
<CardActionArea onClick={() => setOpenSection(section.key)}>
|
|
119
|
-
<CardContent>
|
|
120
|
-
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
|
|
121
|
-
{labelFor(section.key)}
|
|
122
|
-
</Typography>
|
|
123
|
-
<Typography variant="caption" color="text.secondary">
|
|
124
|
-
{translate('tenant.settings.option_count', {
|
|
125
|
-
smart_count: section.entries.length,
|
|
126
|
-
_: `${section.entries.length}`,
|
|
127
|
-
})}
|
|
128
|
-
</Typography>
|
|
129
|
-
</CardContent>
|
|
130
|
-
</CardActionArea>
|
|
131
|
-
</Card>
|
|
132
|
-
))}
|
|
133
|
-
</Box>
|
|
134
|
-
|
|
135
|
-
{sections.map((section) => (
|
|
136
|
-
<Drawer
|
|
137
|
-
key={section.key}
|
|
138
|
-
anchor="right"
|
|
139
|
-
open={openSection === section.key}
|
|
140
|
-
onClose={() => setOpenSection(null)}
|
|
141
|
-
ModalProps={{ keepMounted: true }}
|
|
142
|
-
>
|
|
143
|
-
<Box sx={{ width: { xs: '100vw', sm: 560 }, p: 2 }}>
|
|
144
|
-
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1 }}>
|
|
145
|
-
<Typography variant="h6">{labelFor(section.key)}</Typography>
|
|
146
|
-
<IconButton onClick={() => setOpenSection(null)} size="small" aria-label="Close">
|
|
147
|
-
<CloseIcon />
|
|
148
|
-
</IconButton>
|
|
149
|
-
</Box>
|
|
150
|
-
<Divider sx={{ mb: 2 }} />
|
|
151
|
-
<FormTabsRenderer schema={section.entries} method={method} label={labelFor(section.key)} />
|
|
152
|
-
</Box>
|
|
153
|
-
</Drawer>
|
|
154
|
-
))}
|
|
155
|
-
</section>
|
|
156
|
-
);
|
|
157
|
-
};
|
|
158
27
|
|
|
159
28
|
const TenantSettingsEdit: React.FC<IDashAutoAdminCustomFieldComponent> = ({ method, attribute, tenant }) => {
|
|
160
29
|
const [settingFormatsSchema, setSettingFormatsSchema] = useState<any>(null);
|
package/src/systemResources.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import ResourceTemplate from './templates/ResourceTemplate';
|
|
|
4
4
|
import IAppResourceConfig from './interfaces/IAppResourceConfig';
|
|
5
5
|
import { TenantImpersonateResource } from './resources/Tenant/ImpersonateTenantResource';
|
|
6
6
|
import tenantSystemAdminSchema from './schemas/tenant_superadmin';
|
|
7
|
+
import TenancySettings from './components/tenant/TenancySettings';
|
|
7
8
|
import subscriptionPlanSchema from './schemas/subscriptionPlan';
|
|
8
9
|
import { RutValidator } from './utils/validators';
|
|
9
10
|
import { DASHAppConstants } from 'dash-constants';
|
|
@@ -43,6 +44,21 @@ const systemResources: IAppResourceConfig[] = [
|
|
|
43
44
|
roles: [DASHAppConstants.system.SYSTEM_ROLE],
|
|
44
45
|
trash: true,
|
|
45
46
|
component: ResourceTemplate,
|
|
47
|
+
// Same F21 dynamic-tab pattern the sibling system/tenant resource
|
|
48
|
+
// already uses below — fetches config('tenancies.setting_formats')
|
|
49
|
+
// (core + whatever domain merges onto it, e.g. the AI Providers
|
|
50
|
+
// governance tab) once per edit/view, cached client-side.
|
|
51
|
+
contextComponent: ({ resourceConfig, mode, children }) => {
|
|
52
|
+
return mode === "list" ? children : (
|
|
53
|
+
<SystemRequestsCache
|
|
54
|
+
cacheKey="tenancy_formats_cache"
|
|
55
|
+
apiUrl="system/tenancy/systemSettingFormats"
|
|
56
|
+
cacheSeconds={300}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
</SystemRequestsCache>
|
|
60
|
+
)
|
|
61
|
+
},
|
|
46
62
|
model: "system/tenancy",
|
|
47
63
|
//group: "resource.groups.system_resources",
|
|
48
64
|
group: "Accounts",
|
|
@@ -319,6 +335,17 @@ const systemResources: IAppResourceConfig[] = [
|
|
|
319
335
|
inShow: true,
|
|
320
336
|
inCreate: false,
|
|
321
337
|
},
|
|
338
|
+
{
|
|
339
|
+
tab: 'AI Providers',
|
|
340
|
+
label: 'AI Providers',
|
|
341
|
+
attribute: 'settings', // custom component owns rendering; 'attribute' is required by the interface, not used directly
|
|
342
|
+
type: Object,
|
|
343
|
+
custom: true,
|
|
344
|
+
inList: false,
|
|
345
|
+
component: TenancySettings,
|
|
346
|
+
inCreate: false,
|
|
347
|
+
inShow: false,
|
|
348
|
+
},
|
|
322
349
|
],
|
|
323
350
|
label: "Accounts",
|
|
324
351
|
icon: <BusinessIcon />,
|
|
@@ -364,6 +391,27 @@ const systemResources: IAppResourceConfig[] = [
|
|
|
364
391
|
mutationMode: "pessimistic",
|
|
365
392
|
redirectAfterUpdate: false,
|
|
366
393
|
|
|
394
|
+
// Defensive normalization, same pattern already proven in
|
|
395
|
+
// kt-ecommerce's tenant/tenant resource's own postFormatter: if
|
|
396
|
+
// 'settings' ever ends up as a JS Array (react-hook-form keeps it
|
|
397
|
+
// array-shaped once seeded that way, even after nested dot-path
|
|
398
|
+
// fields like 'settings.bionic_mode' are set on it), JSON.stringify
|
|
399
|
+
// silently drops the non-index keys on submit — the AI Providers tab
|
|
400
|
+
// then looks saved in the UI but nothing persists. The backend now
|
|
401
|
+
// always seeds 'settings' as an object on load (SystemTenancyResource
|
|
402
|
+
// + TenancyController::_postGetOne), which should prevent this at
|
|
403
|
+
// the source, but this stays as a second line of defense — cheap,
|
|
404
|
+
// and it's exactly what already shipped for the sibling resource.
|
|
405
|
+
postFormatter: (params) => {
|
|
406
|
+
if (Array.isArray(params.settings)) {
|
|
407
|
+
params.settings = Object.fromEntries(Object.entries(params.settings));
|
|
408
|
+
}
|
|
409
|
+
if (Array.isArray(params.metadata)) {
|
|
410
|
+
params.metadata = Object.fromEntries(Object.entries(params.metadata));
|
|
411
|
+
}
|
|
412
|
+
return params;
|
|
413
|
+
},
|
|
414
|
+
|
|
367
415
|
...drawerSettings,
|
|
368
416
|
},
|
|
369
417
|
|
|
@@ -434,6 +482,14 @@ const systemResources: IAppResourceConfig[] = [
|
|
|
434
482
|
exporter: false,
|
|
435
483
|
postFormatter: (params) => {
|
|
436
484
|
|
|
485
|
+
// Same array-vs-object defense as system/tenancy's own
|
|
486
|
+
// postFormatter above — see that one's comment for why.
|
|
487
|
+
if (Array.isArray(params.settings)) {
|
|
488
|
+
params.settings = Object.fromEntries(Object.entries(params.settings));
|
|
489
|
+
}
|
|
490
|
+
if (Array.isArray(params.attributes)) {
|
|
491
|
+
params.attributes = Object.fromEntries(Object.entries(params.attributes));
|
|
492
|
+
}
|
|
437
493
|
|
|
438
494
|
if (params.systemMarketplaces) {
|
|
439
495
|
params.system_marketplace_ids = params.systemMarketplaces.map((item) => item.id);
|
|
@@ -463,6 +519,173 @@ const systemResources: IAppResourceConfig[] = [
|
|
|
463
519
|
},
|
|
464
520
|
|
|
465
521
|
|
|
522
|
+
{
|
|
523
|
+
// Platform-wide AI provider credentials (bionic/ollama/deepinfra/
|
|
524
|
+
// aws_bedrock) — see backend App\AiAgentCore\Models\SystemAiProviderKey.
|
|
525
|
+
// The 4 rows are seeded by migration and never created/deleted here;
|
|
526
|
+
// an admin only ever edits an existing row's credentials/flags.
|
|
527
|
+
// Credential VALUES are never returned by the API once saved
|
|
528
|
+
// (SystemAiProviderKeyResource only exposes has_api_key/
|
|
529
|
+
// has_access_key_id/has_secret_access_key) — the fields below are
|
|
530
|
+
// write-only by design, same convention as any masked secret field.
|
|
531
|
+
roles: [DASHAppConstants.system.SYSTEM_ROLE],
|
|
532
|
+
component: ResourceTemplate,
|
|
533
|
+
model: "system/system_ai_provider_key",
|
|
534
|
+
group: "Accounts",
|
|
535
|
+
label: "AI Providers",
|
|
536
|
+
icon: <SystemUpdateAlt />,
|
|
537
|
+
create: false,
|
|
538
|
+
|
|
539
|
+
schema: [
|
|
540
|
+
{
|
|
541
|
+
tab: 'General',
|
|
542
|
+
label: 'Provider',
|
|
543
|
+
attribute: 'provider',
|
|
544
|
+
type: String,
|
|
545
|
+
inList: true,
|
|
546
|
+
inEdit: false,
|
|
547
|
+
inShow: true,
|
|
548
|
+
inCreate: false,
|
|
549
|
+
},
|
|
550
|
+
{
|
|
551
|
+
tab: 'General',
|
|
552
|
+
label: 'Active',
|
|
553
|
+
attribute: 'is_active',
|
|
554
|
+
type: Boolean,
|
|
555
|
+
inList: true,
|
|
556
|
+
inEdit: true,
|
|
557
|
+
inShow: true,
|
|
558
|
+
inCreate: false,
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
tab: 'General',
|
|
562
|
+
label: 'Platform Default Provider',
|
|
563
|
+
attribute: 'is_default',
|
|
564
|
+
type: Boolean,
|
|
565
|
+
inList: true,
|
|
566
|
+
inEdit: true,
|
|
567
|
+
inShow: true,
|
|
568
|
+
inCreate: false,
|
|
569
|
+
fieldProps: {
|
|
570
|
+
helperText: 'The fallback provider used platform-wide, and for any Tenancy that disables a provider without setting its own default.',
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
tab: 'General',
|
|
575
|
+
label: 'API Key',
|
|
576
|
+
attribute: 'credentials.api_key',
|
|
577
|
+
type: String,
|
|
578
|
+
inList: false,
|
|
579
|
+
inEdit: true,
|
|
580
|
+
inShow: false,
|
|
581
|
+
inCreate: false,
|
|
582
|
+
fieldProps: {
|
|
583
|
+
helperText: 'Bionic / Ollama / DeepInfra only. Write-only — leave blank to keep the current value.',
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
tab: 'General',
|
|
588
|
+
label: 'AWS Access Key ID',
|
|
589
|
+
attribute: 'credentials.access_key_id',
|
|
590
|
+
type: String,
|
|
591
|
+
inList: false,
|
|
592
|
+
inEdit: true,
|
|
593
|
+
inShow: false,
|
|
594
|
+
inCreate: false,
|
|
595
|
+
fieldProps: {
|
|
596
|
+
helperText: 'AWS Bedrock only. Write-only — leave blank to keep the current value.',
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
tab: 'General',
|
|
601
|
+
label: 'AWS Secret Access Key',
|
|
602
|
+
attribute: 'credentials.secret_access_key',
|
|
603
|
+
type: String,
|
|
604
|
+
inList: false,
|
|
605
|
+
inEdit: true,
|
|
606
|
+
inShow: false,
|
|
607
|
+
inCreate: false,
|
|
608
|
+
fieldProps: {
|
|
609
|
+
helperText: 'AWS Bedrock only. Write-only — leave blank to keep the current value.',
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
// The platform-wide floor of SystemAiProviderKey::resolveServiceTier()'s
|
|
614
|
+
// 3-level cascade (System -> Tenancy -> Lab) — see
|
|
615
|
+
// vanexa-backend-domain/docs/CASCADING_AI_SETTINGS_KEY_PERMISSIONS.md.
|
|
616
|
+
// Reaches for the SAME static-choices 'select'/'multiselect' types the
|
|
617
|
+
// Tenancy-level "AI Providers" tab already uses (dash-auto-admin's
|
|
618
|
+
// AttributeToInput/AttributeToField) — a hand-written schema like this
|
|
619
|
+
// one is a normal consumer of those types too, not just the dynamic
|
|
620
|
+
// setting_formats pipeline.
|
|
621
|
+
tab: 'General',
|
|
622
|
+
label: 'DeepInfra Service Tier (Platform Default)',
|
|
623
|
+
attribute: 'settings.service_tier',
|
|
624
|
+
type: 'select',
|
|
625
|
+
inList: false,
|
|
626
|
+
inEdit: true,
|
|
627
|
+
inShow: true,
|
|
628
|
+
inCreate: false,
|
|
629
|
+
options: [
|
|
630
|
+
{ id: 'flex', name: 'Flex (0.8x — best-effort, may wait or be rejected)' },
|
|
631
|
+
{ id: 'standard', name: 'Standard (1.0x)' },
|
|
632
|
+
{ id: 'priority', name: 'Priority (1.5x — skips queues)' },
|
|
633
|
+
],
|
|
634
|
+
fieldProps: {
|
|
635
|
+
helperText: 'DeepInfra only. Used whenever a Tenancy has not set its own default.',
|
|
636
|
+
// Matches resolveServiceTier()'s OWN eventual fallback
|
|
637
|
+
// (SystemAiProviderKey::SERVICE_TIER_DEFAULT) — a fresh
|
|
638
|
+
// row with no settings yet shows what would actually be
|
|
639
|
+
// used, not a misleadingly blank field.
|
|
640
|
+
defaultValue: 'standard',
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
tab: 'General',
|
|
645
|
+
label: 'DeepInfra Tiers Any Tenancy May Ever Use',
|
|
646
|
+
attribute: 'settings.service_tier_allowed',
|
|
647
|
+
type: 'multiselect',
|
|
648
|
+
inList: false,
|
|
649
|
+
inEdit: true,
|
|
650
|
+
inShow: true,
|
|
651
|
+
inCreate: false,
|
|
652
|
+
options: [
|
|
653
|
+
{ id: 'flex', name: 'Flex (0.8x — best-effort, may wait or be rejected)' },
|
|
654
|
+
{ id: 'standard', name: 'Standard (1.0x)' },
|
|
655
|
+
{ id: 'priority', name: 'Priority (1.5x — skips queues)' },
|
|
656
|
+
],
|
|
657
|
+
fieldProps: {
|
|
658
|
+
helperText: 'DeepInfra only. The HARD CEILING — a Tenancy\'s own allow-list can only narrow this, never exceed it. Leave all three selected unless a tier should be forbidden platform-wide.',
|
|
659
|
+
// Matches resolveServiceTier()'s own default when this
|
|
660
|
+
// key has never been saved (SystemAiProviderKey::
|
|
661
|
+
// providerSetting()'s $default param, called with
|
|
662
|
+
// SERVICE_TIERS) — an unrestricted ceiling, shown as such
|
|
663
|
+
// rather than as an empty, seemingly-forbid-everything list.
|
|
664
|
+
defaultValue: ['flex', 'standard', 'priority'],
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
],
|
|
668
|
+
|
|
669
|
+
menu: [
|
|
670
|
+
{
|
|
671
|
+
title: 'AI Providers',
|
|
672
|
+
redirect: '/system/system_ai_provider_key',
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
|
|
676
|
+
mutationMode: "pessimistic",
|
|
677
|
+
redirectAfterUpdate: false,
|
|
678
|
+
|
|
679
|
+
drawer: true,
|
|
680
|
+
drawerOptions: {
|
|
681
|
+
view: false,
|
|
682
|
+
edit: true,
|
|
683
|
+
create: false,
|
|
684
|
+
},
|
|
685
|
+
listEditButton: { props: { buttonProps: { variant: 'text', size: 'small' }, label: '' } },
|
|
686
|
+
},
|
|
687
|
+
|
|
688
|
+
|
|
466
689
|
|
|
467
690
|
|
|
468
691
|
|