- Add icanhazstring/composer-unused as dev dependency for dependency hygiene checks - Add German documentation (docs/) covering architecture, conventions, workflows, and developer checklists - Add API layer (ApiAuth, ApiBootstrap, ApiResponse), audit, scheduler, custom fields, and SSO services - Add Microsoft OIDC SSO, API token management, and user lifecycle features - Add swagger-ui vendor integration and OpenAPI spec - Add production Docker setup and bin/ scripts - Update composer dependencies, config, templates, and frontend assets throughout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
182 lines
5.5 KiB
PHTML
182 lines
5.5 KiB
PHTML
<?php
|
|
use MintyPHP\Router;
|
|
|
|
$canCreateTenants = can('tenants.create');
|
|
?>
|
|
<?php
|
|
$breadcrumbs = [
|
|
['label' => t('Home'), 'path' => 'admin'],
|
|
['label' => t('Tenants')],
|
|
];
|
|
require templatePath('partials/app-breadcrumb.phtml');
|
|
?>
|
|
<div class="app-list-titlebar">
|
|
<h1><?php e(t('Tenants')); ?></h1>
|
|
<div class="app-list-titlebar-actions">
|
|
<button
|
|
class="outline secondary"
|
|
type="button"
|
|
data-toolbar-toggle
|
|
data-toolbar-target="#tenants-toolbar"
|
|
data-toolbar-text-show="<?php e(t('Show filters')); ?>"
|
|
data-toolbar-text-hide="<?php e(t('Hide filters')); ?>"
|
|
>
|
|
<i class="bi bi-funnel-fill"></i> <span data-toolbar-label><?php e(t('Hide filters')); ?></span>
|
|
</button>
|
|
<?php if ($canCreateTenants): ?>
|
|
<a role="button" class="primary" href="admin/tenants/create">
|
|
<i class="bi bi-plus"></i> <?php e(t('Create tenant')); ?>
|
|
</a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<div class="app-list-toolbar" id="tenants-toolbar">
|
|
<input type="search" id="tenant-search" placeholder="<?php e(t('Search...')); ?>">
|
|
</div>
|
|
<div class="app-list-table">
|
|
<div id="tenants-grid"></div>
|
|
</div>
|
|
|
|
<script src="<?php e(asset('vendor/gridjs/gridjs.umd.js')); ?>"></script>
|
|
<script type="module">
|
|
import { createServerGrid } from "<?php e(assetVersion('js/core/app-grid-factory.js')); ?>";
|
|
import { badgeHtml, escapeHtml, getAppBase } from "<?php e(asset('js/pages/app-list-utils.js')); ?>";
|
|
|
|
const appBase = getAppBase();
|
|
const statusLabels = {
|
|
active: "<?php e(t('Active')); ?>",
|
|
inactive: "<?php e(t('Inactive')); ?>"
|
|
};
|
|
const labelDefault = "<?php e(t('Default')); ?>";
|
|
const labelMicrosoftOnly = "<?php e(t('Microsoft only')); ?>";
|
|
const labelDisabled = "<?php e(t('Inactive')); ?>";
|
|
|
|
const initTenantsGrid = () => {
|
|
const formatBadge = (value) => badgeHtml(gridjs, value);
|
|
const formatStatus = (value) => {
|
|
const normalized = String(value ?? 'active').toLowerCase();
|
|
const variant = normalized === 'inactive' ? 'danger' : 'success';
|
|
const label = statusLabels[normalized] ?? statusLabels.active;
|
|
return gridjs.html(`<span class="badge" data-variant="${variant}">${label}</span>`);
|
|
};
|
|
const formatTheme = (cell) => {
|
|
if (!cell || typeof cell !== 'object') {
|
|
return gridjs.html('');
|
|
}
|
|
const themeLabel = escapeHtml(cell.label ?? '');
|
|
const label = cell.is_override ? themeLabel : `${themeLabel} (${escapeHtml(labelDefault)})`;
|
|
return gridjs.html(label);
|
|
};
|
|
const formatSso = (cell) => {
|
|
if (!cell || typeof cell !== 'object') {
|
|
return gridjs.html('');
|
|
}
|
|
if (!cell.enabled) {
|
|
return gridjs.html(escapeHtml(labelDisabled));
|
|
}
|
|
if (cell.microsoft_only) {
|
|
return gridjs.html(escapeHtml(labelMicrosoftOnly));
|
|
}
|
|
return gridjs.html(escapeHtml(statusLabels.active));
|
|
};
|
|
const formatCount = (value) => {
|
|
const number = Number(value ?? 0);
|
|
return Number.isFinite(number) ? number : 0;
|
|
};
|
|
const gridConfig = createServerGrid({
|
|
gridjs: window.gridjs,
|
|
container: '#tenants-grid',
|
|
dataUrl: 'admin/tenants/data',
|
|
appBase,
|
|
columns: [
|
|
{
|
|
name: "<?php e(t('Description')); ?>",
|
|
sort: true,
|
|
formatter: (cell) => {
|
|
if (!cell || typeof cell !== 'object') {
|
|
return gridjs.html(String(cell ?? ''));
|
|
}
|
|
const label = cell.label ?? '';
|
|
return gridjs.html(`${label}`);
|
|
}
|
|
},
|
|
{
|
|
name: "<?php e(t('Status')); ?>",
|
|
sort: false,
|
|
formatter: (cell) => formatStatus(cell)
|
|
},
|
|
{
|
|
name: "<?php e(t('Theme')); ?>",
|
|
sort: true,
|
|
formatter: (cell) => formatTheme(cell)
|
|
},
|
|
{
|
|
name: "<?php e(t('SSO')); ?>",
|
|
sort: true,
|
|
formatter: (cell) => formatSso(cell)
|
|
},
|
|
{
|
|
name: "<?php e(t('Active users')); ?>",
|
|
sort: true,
|
|
formatter: (cell) => formatCount(cell)
|
|
},
|
|
{
|
|
name: "<?php e(t('Inactive users')); ?>",
|
|
sort: true,
|
|
formatter: (cell) => formatCount(cell)
|
|
},
|
|
{
|
|
name: "<?php e(t('Created')); ?>",
|
|
sort: true,
|
|
formatter: (cell) => formatBadge(cell)
|
|
},
|
|
{
|
|
name: "UUID",
|
|
hidden: true
|
|
}
|
|
],
|
|
sortColumns: ['description', 'status', 'theme', 'sso', 'active_users', 'inactive_users', 'created', null],
|
|
paginationLimit: 10,
|
|
language: <?php MintyPHP\Buffer::get('grid_lang'); ?>,
|
|
mapData: (data) => data.data.map((row) => [
|
|
{
|
|
label: row.description,
|
|
is_default: row.is_default ? 1 : 0
|
|
},
|
|
row.status,
|
|
row.theme ?? null,
|
|
row.sso ?? null,
|
|
row.active_users ?? 0,
|
|
row.inactive_users ?? 0,
|
|
row.created,
|
|
row.uuid
|
|
]),
|
|
actions: { enabled: false },
|
|
search: {
|
|
input: '#tenant-search',
|
|
param: 'search',
|
|
debounce: 250
|
|
},
|
|
urlSync: true,
|
|
rowDataset: (row) => ({
|
|
uuid: row?.cells?.[7]?.data
|
|
}),
|
|
rowDblClick: {
|
|
getUrl: (rowData) => {
|
|
const uuid = rowData?.cells?.[7]?.data;
|
|
return uuid ? new URL(`admin/tenants/edit/${uuid}`, appBase).toString() : '';
|
|
}
|
|
}
|
|
});
|
|
|
|
if (!gridConfig || !gridConfig.grid) {
|
|
console.warn('Tenants grid init failed');
|
|
return null;
|
|
}
|
|
|
|
return gridConfig;
|
|
};
|
|
|
|
initTenantsGrid();
|
|
</script>
|