simple-module-feature-flags 0.0.30__tar.gz → 0.0.31__tar.gz
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.
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/.gitignore +1 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/PKG-INFO +5 -5
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/locales/en.json +3 -4
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/module.py +3 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/pages/Browse.tsx +6 -52
- simple_module_feature_flags-0.0.31/feature_flags/pages/components/TenantPicker.tsx +112 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/pyproject.toml +4 -4
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/LICENSE +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/README.md +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/__init__.py +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/constants.py +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/contracts/__init__.py +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/contracts/schemas.py +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/deps.py +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/endpoints/__init__.py +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/endpoints/api.py +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/endpoints/views.py +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/models.py +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/py.typed +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/service.py +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/package.json +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/tests/test_feature_flags_api.py +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/tests/test_feature_flags_service.py +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/tests/test_store_outage_fallback.py +0 -0
- {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_feature_flags
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.31
|
|
4
4
|
Summary: Simple feature-flag module with per-tenant overrides and a consumer API for simple_module
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -21,9 +21,9 @@ Classifier: Topic :: Internet :: WWW/HTTP
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.12
|
|
24
|
-
Requires-Dist: simple-module-core==0.0.
|
|
25
|
-
Requires-Dist: simple-module-db==0.0.
|
|
26
|
-
Requires-Dist: simple-module-hosting==0.0.
|
|
24
|
+
Requires-Dist: simple-module-core==0.0.31
|
|
25
|
+
Requires-Dist: simple-module-db==0.0.31
|
|
26
|
+
Requires-Dist: simple-module-hosting==0.0.31
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
28
|
|
|
29
29
|
# simple_module_feature_flags
|
|
@@ -10,12 +10,11 @@
|
|
|
10
10
|
"viewing_tenant": "Viewing overrides for tenant \"{tenant_id}\". Tenant overrides beat the system value.",
|
|
11
11
|
"tenant_id_label": "Tenant ID",
|
|
12
12
|
"tenant_id_placeholder": "e.g. acme",
|
|
13
|
-
"scope_label": "Scope",
|
|
14
13
|
"scope_system": "System (all tenants)",
|
|
15
|
-
"scope_tenant": "Specific tenant",
|
|
16
14
|
"go": "View",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
15
|
+
"viewing_label": "Viewing",
|
|
16
|
+
"scope_custom": "Other tenant…",
|
|
17
|
+
"cancel": "Cancel"
|
|
19
18
|
},
|
|
20
19
|
"table": {
|
|
21
20
|
"name": "Flag",
|
{simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/module.py
RENAMED
|
@@ -48,6 +48,9 @@ class FeatureFlagsModule(ModuleBase):
|
|
|
48
48
|
order=MENU_ORDER,
|
|
49
49
|
section=MenuSection.SIDEBAR,
|
|
50
50
|
group="Administration",
|
|
51
|
+
# Mirrors the view router's guard. Without it the entry shows
|
|
52
|
+
# for every signed-in account and 403s on click.
|
|
53
|
+
permissions=[PERM_FEATURE_FLAGS_VIEW],
|
|
51
54
|
)
|
|
52
55
|
)
|
|
53
56
|
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
EmptyMedia,
|
|
11
11
|
EmptyTitle,
|
|
12
12
|
} from '@simple-module-py/ui/components/ui/empty';
|
|
13
|
-
import { Input } from '@simple-module-py/ui/components/ui/input';
|
|
14
13
|
import { Switch } from '@simple-module-py/ui/components/ui/switch';
|
|
15
14
|
import {
|
|
16
15
|
Table,
|
|
@@ -23,8 +22,8 @@ import {
|
|
|
23
22
|
import { usePermissions } from '@simple-module-py/ui/hooks/use-permissions';
|
|
24
23
|
import { AuthenticatedLayout } from '@simple-module-py/ui/layouts/AuthenticatedLayout';
|
|
25
24
|
import { Flag, RotateCcw } from 'lucide-react';
|
|
26
|
-
import { useState } from 'react';
|
|
27
25
|
import { toast } from 'sonner';
|
|
26
|
+
import { TenantPicker } from './components/TenantPicker';
|
|
28
27
|
|
|
29
28
|
interface FeatureFlag {
|
|
30
29
|
name: string;
|
|
@@ -55,7 +54,6 @@ function Browse() {
|
|
|
55
54
|
const { t } = useT();
|
|
56
55
|
const { can } = usePermissions();
|
|
57
56
|
const canManage = can('feature_flags.manage');
|
|
58
|
-
const [tenantInput, setTenantInput] = useState(tenant_id ?? '');
|
|
59
57
|
|
|
60
58
|
function handleToggle(flag: FeatureFlag, next: boolean) {
|
|
61
59
|
router.post(
|
|
@@ -86,10 +84,6 @@ function Browse() {
|
|
|
86
84
|
);
|
|
87
85
|
}
|
|
88
86
|
|
|
89
|
-
function visitTenant(value: string) {
|
|
90
|
-
router.visit(buildPath(value.trim() || null));
|
|
91
|
-
}
|
|
92
|
-
|
|
93
87
|
return (
|
|
94
88
|
<>
|
|
95
89
|
<Head title="Feature Flags" />
|
|
@@ -98,51 +92,11 @@ function Browse() {
|
|
|
98
92
|
description={t(keys.feature_flags.browse.description)}
|
|
99
93
|
>
|
|
100
94
|
<Card className="mb-4 p-4">
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}}
|
|
107
|
-
>
|
|
108
|
-
<div className="flex-1 min-w-[200px]">
|
|
109
|
-
<label className="block text-sm font-medium mb-1" htmlFor="tenant_id">
|
|
110
|
-
{t(keys.feature_flags.browse.tenant_id_label)}
|
|
111
|
-
</label>
|
|
112
|
-
<Input
|
|
113
|
-
id="tenant_id"
|
|
114
|
-
value={tenantInput}
|
|
115
|
-
onChange={(e) => setTenantInput(e.target.value)}
|
|
116
|
-
placeholder={t(keys.feature_flags.browse.tenant_id_placeholder)}
|
|
117
|
-
/>
|
|
118
|
-
</div>
|
|
119
|
-
<Button type="submit" variant="default">
|
|
120
|
-
{t(keys.feature_flags.browse.go)}
|
|
121
|
-
</Button>
|
|
122
|
-
{tenant_id && (
|
|
123
|
-
<Button type="button" variant="ghost" onClick={() => visitTenant('')}>
|
|
124
|
-
{t(keys.feature_flags.browse.back_to_system)}
|
|
125
|
-
</Button>
|
|
126
|
-
)}
|
|
127
|
-
</form>
|
|
128
|
-
{tenants.length > 0 && (
|
|
129
|
-
<div className="mt-3 flex flex-wrap items-center gap-2 text-sm">
|
|
130
|
-
<span className="text-muted-foreground">
|
|
131
|
-
{t(keys.feature_flags.browse.tenants_with_overrides)}:
|
|
132
|
-
</span>
|
|
133
|
-
{tenants.map((tid) => (
|
|
134
|
-
<Button
|
|
135
|
-
key={tid}
|
|
136
|
-
type="button"
|
|
137
|
-
size="sm"
|
|
138
|
-
variant={tid === tenant_id ? 'default' : 'outline'}
|
|
139
|
-
onClick={() => visitTenant(tid)}
|
|
140
|
-
>
|
|
141
|
-
{tid}
|
|
142
|
-
</Button>
|
|
143
|
-
))}
|
|
144
|
-
</div>
|
|
145
|
-
)}
|
|
95
|
+
<TenantPicker
|
|
96
|
+
tenantId={tenant_id}
|
|
97
|
+
tenants={tenants}
|
|
98
|
+
onSelect={(next) => router.visit(buildPath(next))}
|
|
99
|
+
/>
|
|
146
100
|
<p className="mt-3 text-sm text-muted-foreground">
|
|
147
101
|
{tenant_id
|
|
148
102
|
? t(keys.feature_flags.browse.viewing_tenant, { tenant_id })
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { keys, useT } from '@simple-module-py/i18n';
|
|
2
|
+
import { Button } from '@simple-module-py/ui/components/ui/button';
|
|
3
|
+
import { Input } from '@simple-module-py/ui/components/ui/input';
|
|
4
|
+
import {
|
|
5
|
+
Select,
|
|
6
|
+
SelectContent,
|
|
7
|
+
SelectItem,
|
|
8
|
+
SelectSeparator,
|
|
9
|
+
SelectTrigger,
|
|
10
|
+
SelectValue,
|
|
11
|
+
} from '@simple-module-py/ui/components/ui/select';
|
|
12
|
+
import { useState } from 'react';
|
|
13
|
+
|
|
14
|
+
interface Props {
|
|
15
|
+
/** Currently-viewed tenant, or null for the system scope. */
|
|
16
|
+
tenantId: string | null;
|
|
17
|
+
/** Tenants that already carry at least one override. */
|
|
18
|
+
tenants: string[];
|
|
19
|
+
onSelect: (tenantId: string | null) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SYSTEM = '__system__';
|
|
23
|
+
const CUSTOM = '__custom__';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Scope picker for the flags table.
|
|
27
|
+
*
|
|
28
|
+
* This was a free-text box, so viewing a tenant meant knowing and correctly
|
|
29
|
+
* typing its id — and a typo silently showed a scope with no overrides
|
|
30
|
+
* instead of an error.
|
|
31
|
+
*
|
|
32
|
+
* The list cannot be closed: there is no tenant registry in the framework
|
|
33
|
+
* (ids arrive on auth claims), so the only tenants this app can enumerate are
|
|
34
|
+
* those that already have an override. Creating the *first* override for a
|
|
35
|
+
* tenant therefore still needs a way to name one by hand, which is what the
|
|
36
|
+
* "other tenant" branch is for.
|
|
37
|
+
*/
|
|
38
|
+
export function TenantPicker({ tenantId, tenants, onSelect }: Props) {
|
|
39
|
+
const { t } = useT();
|
|
40
|
+
const [custom, setCustom] = useState(false);
|
|
41
|
+
const [draft, setDraft] = useState('');
|
|
42
|
+
|
|
43
|
+
// The tenant being viewed may not have overrides yet, so it can be absent
|
|
44
|
+
// from `tenants` — without this it would vanish from its own picker.
|
|
45
|
+
const options = [...new Set(tenantId ? [...tenants, tenantId] : tenants)].sort();
|
|
46
|
+
|
|
47
|
+
function handleChange(value: string) {
|
|
48
|
+
if (value === CUSTOM) {
|
|
49
|
+
setCustom(true);
|
|
50
|
+
setDraft('');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setCustom(false);
|
|
54
|
+
onSelect(value === SYSTEM ? null : value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex flex-wrap items-end gap-3">
|
|
59
|
+
<div className="min-w-[240px]">
|
|
60
|
+
<span className="mb-1 block text-sm font-medium">
|
|
61
|
+
{t(keys.feature_flags.browse.viewing_label)}
|
|
62
|
+
</span>
|
|
63
|
+
<Select value={custom ? CUSTOM : (tenantId ?? SYSTEM)} onValueChange={handleChange}>
|
|
64
|
+
<SelectTrigger className="w-full">
|
|
65
|
+
<SelectValue />
|
|
66
|
+
</SelectTrigger>
|
|
67
|
+
<SelectContent>
|
|
68
|
+
<SelectItem value={SYSTEM}>{t(keys.feature_flags.browse.scope_system)}</SelectItem>
|
|
69
|
+
{options.length > 0 && <SelectSeparator />}
|
|
70
|
+
{options.map((tid) => (
|
|
71
|
+
<SelectItem key={tid} value={tid}>
|
|
72
|
+
{tid}
|
|
73
|
+
</SelectItem>
|
|
74
|
+
))}
|
|
75
|
+
<SelectSeparator />
|
|
76
|
+
<SelectItem value={CUSTOM}>{t(keys.feature_flags.browse.scope_custom)}</SelectItem>
|
|
77
|
+
</SelectContent>
|
|
78
|
+
</Select>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{custom && (
|
|
82
|
+
<form
|
|
83
|
+
className="flex items-end gap-2"
|
|
84
|
+
onSubmit={(e) => {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
const trimmed = draft.trim();
|
|
87
|
+
if (trimmed) onSelect(trimmed);
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
<div className="min-w-[200px]">
|
|
91
|
+
<label className="mb-1 block text-sm font-medium" htmlFor="tenant_id">
|
|
92
|
+
{t(keys.feature_flags.browse.tenant_id_label)}
|
|
93
|
+
</label>
|
|
94
|
+
<Input
|
|
95
|
+
id="tenant_id"
|
|
96
|
+
autoFocus
|
|
97
|
+
value={draft}
|
|
98
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
99
|
+
placeholder={t(keys.feature_flags.browse.tenant_id_placeholder)}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
<Button type="submit" disabled={!draft.trim()}>
|
|
103
|
+
{t(keys.feature_flags.browse.go)}
|
|
104
|
+
</Button>
|
|
105
|
+
<Button type="button" variant="ghost" onClick={() => setCustom(false)}>
|
|
106
|
+
{t(keys.feature_flags.browse.cancel)}
|
|
107
|
+
</Button>
|
|
108
|
+
</form>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_feature_flags"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.31"
|
|
4
4
|
description = "Simple feature-flag module with per-tenant overrides and a consumer API for simple_module"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -21,9 +21,9 @@ classifiers = [
|
|
|
21
21
|
"Typing :: Typed",
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
|
-
"simple_module_core==0.0.
|
|
25
|
-
"simple_module_db==0.0.
|
|
26
|
-
"simple_module_hosting==0.0.
|
|
24
|
+
"simple_module_core==0.0.31",
|
|
25
|
+
"simple_module_db==0.0.31",
|
|
26
|
+
"simple_module_hosting==0.0.31",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[project.entry-points.simple_module]
|
|
File without changes
|
|
File without changes
|
{simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/__init__.py
RENAMED
|
File without changes
|
{simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/constants.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/deps.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/models.py
RENAMED
|
File without changes
|
{simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/py.typed
RENAMED
|
File without changes
|
{simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|