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.
Files changed (25) hide show
  1. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/.gitignore +1 -0
  2. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/PKG-INFO +5 -5
  3. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/locales/en.json +3 -4
  4. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/module.py +3 -0
  5. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/pages/Browse.tsx +6 -52
  6. simple_module_feature_flags-0.0.31/feature_flags/pages/components/TenantPicker.tsx +112 -0
  7. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/pyproject.toml +4 -4
  8. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/LICENSE +0 -0
  9. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/README.md +0 -0
  10. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/__init__.py +0 -0
  11. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/constants.py +0 -0
  12. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/contracts/__init__.py +0 -0
  13. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/contracts/schemas.py +0 -0
  14. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/deps.py +0 -0
  15. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/endpoints/__init__.py +0 -0
  16. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/endpoints/api.py +0 -0
  17. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/endpoints/views.py +0 -0
  18. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/models.py +0 -0
  19. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/py.typed +0 -0
  20. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/feature_flags/service.py +0 -0
  21. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/package.json +0 -0
  22. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/tests/test_feature_flags_api.py +0 -0
  23. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/tests/test_feature_flags_service.py +0 -0
  24. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/tests/test_store_outage_fallback.py +0 -0
  25. {simple_module_feature_flags-0.0.30 → simple_module_feature_flags-0.0.31}/tsconfig.json +0 -0
@@ -63,3 +63,4 @@ Thumbs.db
63
63
  host/client_app/.playwright-cli/*
64
64
  .superpowers/
65
65
  .qa/
66
+ .verify/
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: simple_module_feature_flags
3
- Version: 0.0.30
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.30
25
- Requires-Dist: simple-module-db==0.0.30
26
- Requires-Dist: simple-module-hosting==0.0.30
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
- "back_to_system": "Back to system view",
18
- "tenants_with_overrides": "Tenants with overrides"
15
+ "viewing_label": "Viewing",
16
+ "scope_custom": "Other tenant…",
17
+ "cancel": "Cancel"
19
18
  },
20
19
  "table": {
21
20
  "name": "Flag",
@@ -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
- <form
102
- className="flex flex-wrap items-end gap-3"
103
- onSubmit={(e) => {
104
- e.preventDefault();
105
- visitTenant(tenantInput);
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.30"
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.30",
25
- "simple_module_db==0.0.30",
26
- "simple_module_hosting==0.0.30",
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]