@featurevisor/site 0.8.0

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 (70) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +21 -0
  3. package/README.md +15 -0
  4. package/dist/index.css +2183 -0
  5. package/dist/index.js +3 -0
  6. package/dist/index.js.LICENSE.txt +68 -0
  7. package/dist/index.js.map +1 -0
  8. package/lib/components/Alert.d.ts +7 -0
  9. package/lib/components/App.d.ts +2 -0
  10. package/lib/components/EditLink.d.ts +2 -0
  11. package/lib/components/EnvironmentDot.d.ts +8 -0
  12. package/lib/components/ExpandConditions.d.ts +6 -0
  13. package/lib/components/ExpandRuleSegments.d.ts +6 -0
  14. package/lib/components/Footer.d.ts +2 -0
  15. package/lib/components/Header.d.ts +2 -0
  16. package/lib/components/HistoryTimeline.d.ts +10 -0
  17. package/lib/components/LastModified.d.ts +2 -0
  18. package/lib/components/ListAttributes.d.ts +2 -0
  19. package/lib/components/ListFeatures.d.ts +2 -0
  20. package/lib/components/ListHistory.d.ts +2 -0
  21. package/lib/components/ListSegments.d.ts +2 -0
  22. package/lib/components/Markdown.d.ts +2 -0
  23. package/lib/components/PageContent.d.ts +4 -0
  24. package/lib/components/PageTitle.d.ts +5 -0
  25. package/lib/components/PrettyDate.d.ts +7 -0
  26. package/lib/components/SearchInput.d.ts +7 -0
  27. package/lib/components/ShowAttribute.d.ts +5 -0
  28. package/lib/components/ShowFeature.d.ts +10 -0
  29. package/lib/components/ShowSegment.d.ts +5 -0
  30. package/lib/components/Tabs.d.ts +10 -0
  31. package/lib/components/Tag.d.ts +6 -0
  32. package/lib/contexts/SearchIndexContext.d.ts +7 -0
  33. package/lib/hooks/searchIndexHook.d.ts +1 -0
  34. package/lib/index.d.ts +1 -0
  35. package/lib/utils/index.d.ts +23 -0
  36. package/package.json +62 -0
  37. package/public/favicon-128.png +0 -0
  38. package/public/index.html +15 -0
  39. package/src/components/Alert.tsx +22 -0
  40. package/src/components/App.tsx +129 -0
  41. package/src/components/EditLink.tsx +18 -0
  42. package/src/components/EnvironmentDot.tsx +48 -0
  43. package/src/components/ExpandConditions.tsx +84 -0
  44. package/src/components/ExpandRuleSegments.tsx +59 -0
  45. package/src/components/Footer.tsx +14 -0
  46. package/src/components/Header.tsx +62 -0
  47. package/src/components/HistoryTimeline.tsx +179 -0
  48. package/src/components/LastModified.tsx +29 -0
  49. package/src/components/ListAttributes.tsx +87 -0
  50. package/src/components/ListFeatures.tsx +95 -0
  51. package/src/components/ListHistory.tsx +17 -0
  52. package/src/components/ListSegments.tsx +81 -0
  53. package/src/components/Markdown.tsx +11 -0
  54. package/src/components/PageContent.tsx +9 -0
  55. package/src/components/PageTitle.tsx +23 -0
  56. package/src/components/PrettyDate.tsx +56 -0
  57. package/src/components/SearchInput.tsx +34 -0
  58. package/src/components/ShowAttribute.tsx +145 -0
  59. package/src/components/ShowFeature.tsx +490 -0
  60. package/src/components/ShowSegment.tsx +125 -0
  61. package/src/components/Tabs.tsx +46 -0
  62. package/src/components/Tag.tsx +13 -0
  63. package/src/contexts/SearchIndexContext.tsx +13 -0
  64. package/src/hooks/searchIndexHook.ts +9 -0
  65. package/src/index.css +8 -0
  66. package/src/index.tsx +12 -0
  67. package/src/utils/index.ts +203 -0
  68. package/tailwind.config.js +8 -0
  69. package/tsconfig.cjs.json +7 -0
  70. package/webpack.config.js +14 -0
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+
3
+ interface SearchInputProps {
4
+ value: string;
5
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
6
+ }
7
+
8
+ export function SearchInput(props: SearchInputProps) {
9
+ return (
10
+ <div className="relative px-6 pt-3.5">
11
+ <div className="pointer-events-none absolute">
12
+ <svg
13
+ className="absolute ml-3 mt-5 h-6 w-6 text-slate-400"
14
+ viewBox="0 0 20 20"
15
+ fill="currentColor"
16
+ >
17
+ <path
18
+ fill-rule="evenodd"
19
+ d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
20
+ clip-rule="evenodd"
21
+ />
22
+ </svg>
23
+ </div>
24
+ <input
25
+ type="text"
26
+ value={props.value}
27
+ onChange={(e) => props.onChange(e)}
28
+ placeholder="Type to search..."
29
+ autoComplete="off"
30
+ className="mb-4 mt-2 w-full rounded-full border-slate-300 indent-8 text-xl text-gray-700 placeholder:text-gray-400"
31
+ />
32
+ </div>
33
+ );
34
+ }
@@ -0,0 +1,145 @@
1
+ import * as React from "react";
2
+ import { useParams, Outlet, useOutletContext, Link } from "react-router-dom";
3
+
4
+ import { PageContent } from "./PageContent";
5
+ import { PageTitle } from "./PageTitle";
6
+ import { Tabs } from "./Tabs";
7
+ import { EditLink } from "./EditLink";
8
+ import { useSearchIndex } from "../hooks/searchIndexHook";
9
+ import { Markdown } from "./Markdown";
10
+ import { HistoryTimeline } from "./HistoryTimeline";
11
+
12
+ export function DisplayAttributeOverview() {
13
+ const { attribute } = useOutletContext() as any;
14
+
15
+ return (
16
+ <div className="border-gray-200 py-6">
17
+ <dl className="grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2">
18
+ <div>
19
+ <dt className="text-sm font-medium text-gray-500">Key</dt>
20
+ <dd className="mt-1 text-sm text-gray-900">{attribute.key}</dd>
21
+ </div>
22
+ <div>
23
+ <dt className="text-sm font-medium text-gray-500">Archived</dt>
24
+ <dd className="mt-1 text-sm text-gray-900">
25
+ {attribute.archived === true ? <span>Yes</span> : <span>No</span>}
26
+ </dd>
27
+ </div>
28
+ <div>
29
+ <dt className="text-sm font-medium text-gray-500">Type</dt>
30
+ <dd className="mt-1 text-sm text-gray-900">{attribute.type}</dd>
31
+ </div>
32
+ <div>
33
+ <dt className="text-sm font-medium text-gray-500">Capture</dt>
34
+ <dd className="mt-1 text-sm text-gray-900">
35
+ {attribute.capture === true ? <span>Yes</span> : <span>No</span>}
36
+ </dd>
37
+ </div>
38
+ <div className="col-span-2">
39
+ <dt className="text-sm font-medium text-gray-500">Description</dt>
40
+ <dd className="mt-1 text-sm text-gray-900">
41
+ {attribute.description.trim().length > 0 ? (
42
+ <Markdown children={attribute.description} />
43
+ ) : (
44
+ "n/a"
45
+ )}
46
+ </dd>
47
+ </div>
48
+ </dl>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ export function DisplayAttributeUsage() {
54
+ const { attribute } = useOutletContext() as any;
55
+
56
+ return (
57
+ <div className="border-gray-200 py-6">
58
+ <dl className="grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2">
59
+ <div>
60
+ <dt className="text-sm font-medium text-gray-500">Segments</dt>
61
+ <dd className="mt-1 text-sm text-gray-900">
62
+ {attribute.usedInSegments.length === 0 && "none"}
63
+
64
+ {attribute.usedInSegments.length > 0 && (
65
+ <ul className="list-inside list-disc">
66
+ {attribute.usedInSegments.map((segment) => {
67
+ return (
68
+ <li key={segment}>
69
+ <Link to={`/segments/${segment}`}>{segment}</Link>
70
+ </li>
71
+ );
72
+ })}
73
+ </ul>
74
+ )}
75
+ </dd>
76
+ </div>
77
+ <div>
78
+ <dt className="text-sm font-medium text-gray-500">Features</dt>
79
+ <dd className="mt-1 text-sm text-gray-900">
80
+ {attribute.usedInFeatures.length === 0 && "none"}
81
+
82
+ {attribute.usedInFeatures.length > 0 && (
83
+ <ul className="list-inside list-disc">
84
+ {attribute.usedInFeatures.map((feature) => {
85
+ return (
86
+ <li key={feature}>
87
+ <Link to={`/features/${feature}`}>{feature}</Link>
88
+ </li>
89
+ );
90
+ })}
91
+ </ul>
92
+ )}
93
+ </dd>
94
+ </div>
95
+ </dl>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ export function DisplayAttributeHistory() {
101
+ const { attribute } = useOutletContext() as any;
102
+
103
+ return <HistoryTimeline entityType="attribute" entityKey={attribute.key} />;
104
+ }
105
+
106
+ export function ShowAttribute(props) {
107
+ const { attributeKey } = useParams();
108
+ const { data } = useSearchIndex();
109
+ const links = data?.links;
110
+ const attribute = data?.entities.attributes.find((a) => a.key === attributeKey);
111
+
112
+ if (!attribute) {
113
+ return <p>Attribute not found.</p>;
114
+ }
115
+
116
+ const tabs = [
117
+ {
118
+ title: "Overview",
119
+ to: `/attributes/${attributeKey}`,
120
+ },
121
+ {
122
+ title: "Usage",
123
+ to: `/attributes/${attributeKey}/usage`,
124
+ },
125
+ {
126
+ title: "History",
127
+ to: `/attributes/${attributeKey}/history`,
128
+ },
129
+ ];
130
+
131
+ return (
132
+ <PageContent>
133
+ <PageTitle className="border-none">
134
+ Attribute: {attributeKey}{" "}
135
+ {links && <EditLink url={links.attribute.replace("{{key}}", attribute.key)} />}
136
+ </PageTitle>
137
+
138
+ <Tabs tabs={tabs} />
139
+
140
+ <div className="p-8">
141
+ <Outlet context={{ attribute }} />
142
+ </div>
143
+ </PageContent>
144
+ );
145
+ }
@@ -0,0 +1,490 @@
1
+ import * as React from "react";
2
+ import { useParams, useOutletContext, Outlet, NavLink } from "react-router-dom";
3
+
4
+ import { PageContent } from "./PageContent";
5
+ import { PageTitle } from "./PageTitle";
6
+ import { Tabs } from "./Tabs";
7
+ import { HistoryTimeline } from "./HistoryTimeline";
8
+ import { Tag } from "./Tag";
9
+ import { useSearchIndex } from "../hooks/searchIndexHook";
10
+ import { isEnabledInEnvironment } from "../utils";
11
+ import { ExpandRuleSegments } from "./ExpandRuleSegments";
12
+ import { ExpandConditions } from "./ExpandConditions";
13
+ import { EditLink } from "./EditLink";
14
+ import { Markdown } from "./Markdown";
15
+
16
+ export function DisplayFeatureOverview() {
17
+ const { feature } = useOutletContext() as any;
18
+
19
+ const environmentKeys = Object.keys(feature.environments).sort();
20
+
21
+ return (
22
+ <div className="border-gray-200">
23
+ <dl className="grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2">
24
+ <div>
25
+ <dt className="text-sm font-medium text-gray-500">Key</dt>
26
+ <dd className="mt-1 text-sm text-gray-900">{feature.key}</dd>
27
+ </div>
28
+ <div>
29
+ <dt className="text-sm font-medium text-gray-500">Archived</dt>
30
+ <dd className="mt-1 text-sm text-gray-900">
31
+ {feature.archived === true ? <span>Yes</span> : <span>No</span>}
32
+ </dd>
33
+ </div>
34
+ <div>
35
+ <dt className="text-sm font-medium text-gray-500">Default variation</dt>
36
+ <dd className="mt-1 text-sm text-gray-900">
37
+ {typeof feature.defaultVariation === "string" ? (
38
+ <span>{feature.defaultVariation}</span>
39
+ ) : (
40
+ <pre>
41
+ <code className="rounded bg-gray-100 px-2 py-1 text-red-400">
42
+ {JSON.stringify(feature.defaultVariation)}
43
+ </code>
44
+ </pre>
45
+ )}
46
+ </dd>
47
+ </div>
48
+ <div>
49
+ <dt className="text-sm font-medium text-gray-500">Bucket by</dt>
50
+ <dd className="mt-1 text-sm text-gray-900">
51
+ {typeof feature.bucketBy === "string" ? (
52
+ <span>{feature.bucketBy}</span>
53
+ ) : (
54
+ <ul>
55
+ {feature.bucketBy.map((b) => (
56
+ <li key={b}>{b}</li>
57
+ ))}
58
+ </ul>
59
+ )}
60
+ </dd>
61
+ </div>
62
+ <div>
63
+ <dt className="text-sm font-medium text-gray-500">Status</dt>
64
+ <dd className="mt-1 text-sm text-gray-900">
65
+ <ul className="">
66
+ {environmentKeys.map((environmentKey) => (
67
+ <li key={environmentKey}>
68
+ <span className="relative top-0.5 inline-block h-3 w-3">
69
+ {isEnabledInEnvironment(feature, environmentKey) ? (
70
+ <span className="relative inline-block h-3 w-3 rounded-full bg-green-500"></span>
71
+ ) : (
72
+ <span className="relative inline-block h-3 w-3 rounded-full bg-slate-300"></span>
73
+ )}
74
+ </span>{" "}
75
+ {environmentKey}
76
+ </li>
77
+ ))}
78
+ </ul>
79
+ </dd>
80
+ </div>
81
+ <div>
82
+ <dt className="text-sm font-medium text-gray-500">Tags</dt>
83
+ <dd className="mt-1 text-sm text-gray-900">
84
+ {feature.tags.map((tag: string) => (
85
+ <Tag tag={tag} key={tag} />
86
+ ))}
87
+ </dd>
88
+ </div>
89
+ <div className="col-span-2">
90
+ <dt className="text-sm font-medium text-gray-500">Description</dt>
91
+ <dd className="mt-1 text-sm text-gray-900">
92
+ {feature.description && feature.description.trim().length > 0 ? (
93
+ <Markdown children={feature.description} />
94
+ ) : (
95
+ "n/a"
96
+ )}
97
+ </dd>
98
+ </div>
99
+ </dl>
100
+ </div>
101
+ );
102
+ }
103
+
104
+ export function DisplayFeatureForceTable() {
105
+ const { feature } = useOutletContext() as any;
106
+ const { environmentKey } = useParams();
107
+
108
+ if (
109
+ !environmentKey ||
110
+ !feature.environments[environmentKey] ||
111
+ !feature.environments[environmentKey].force
112
+ ) {
113
+ return <p>n/a</p>;
114
+ }
115
+
116
+ return (
117
+ <table className="mt-3 min-w-full divide-y divide-gray-300 border border-gray-200">
118
+ <thead className="bg-gray-50">
119
+ <tr>
120
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">
121
+ Conditions / Segments
122
+ </th>
123
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">
124
+ Variation
125
+ </th>
126
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">
127
+ Variables
128
+ </th>
129
+ </tr>
130
+ </thead>
131
+
132
+ <tbody>
133
+ {feature.environments[environmentKey].force.map((force, index) => {
134
+ return (
135
+ <tr key={index} className={index % 2 === 0 ? undefined : "bg-gray-50"}>
136
+ <td className="py-4 pl-4 pr-3 text-sm text-gray-900">
137
+ {force.conditions ? (
138
+ <ExpandConditions conditions={force.conditions} />
139
+ ) : (
140
+ <ExpandRuleSegments segments={force.segments} />
141
+ )}
142
+ </td>
143
+ <td className="py-4 pl-4 pr-3 text-sm text-gray-900">
144
+ {force.variation && typeof force.variation === "string" && (
145
+ <span>{force.variation}</span>
146
+ )}
147
+
148
+ {force.variation && typeof force.variation != "string" && (
149
+ <code className="rounded bg-gray-100 px-2 py-1 text-red-400">
150
+ {JSON.stringify(force.variation)}
151
+ </code>
152
+ )}
153
+ </td>
154
+ <td className="py-4 pl-4 pr-3 text-sm text-gray-900">
155
+ {force.variables && (
156
+ <ul className="list-inside list-disc">
157
+ {Object.keys(force.variables).map((k) => {
158
+ return (
159
+ <li key={k}>
160
+ <span className="font-semibold">{k}</span>:{" "}
161
+ {typeof force.variables[k] === "string" && force.variables[k]}
162
+ {typeof force.variables[k] !== "string" && (
163
+ <code className="rounded bg-gray-100 px-2 py-1 text-red-400">
164
+ {force.variables[k]}
165
+ </code>
166
+ )}
167
+ </li>
168
+ );
169
+ })}
170
+ </ul>
171
+ )}
172
+ </td>
173
+ </tr>
174
+ );
175
+ })}
176
+ </tbody>
177
+ </table>
178
+ );
179
+ }
180
+
181
+ export function DisplayFeatureForce() {
182
+ const { feature } = useOutletContext() as any;
183
+ const environmentKeys = Object.keys(feature.environments).sort();
184
+
185
+ const environmentTabs = environmentKeys.map((environmentKey, index) => {
186
+ return {
187
+ title: environmentKey,
188
+ to: `/features/${feature.key}/force/${environmentKey}`,
189
+ };
190
+ });
191
+
192
+ return (
193
+ <>
194
+ <nav className="flex space-x-4" aria-label="Tabs">
195
+ {environmentTabs.map((tab) => (
196
+ <NavLink
197
+ key={tab.title}
198
+ to={tab.to}
199
+ className={({ isActive }) =>
200
+ [
201
+ isActive ? "bg-gray-200 text-gray-800" : "text-gray-600 hover:text-gray-800",
202
+ "rounded-md px-3 py-2 text-sm font-medium",
203
+ ].join(" ")
204
+ }
205
+ >
206
+ {tab.title}
207
+ </NavLink>
208
+ ))}
209
+ </nav>
210
+
211
+ <Outlet context={{ feature }} />
212
+ </>
213
+ );
214
+ }
215
+
216
+ export function DisplayFeatureRulesTable() {
217
+ const { feature } = useOutletContext() as any;
218
+ const { environmentKey } = useParams();
219
+
220
+ if (!environmentKey || !feature.environments[environmentKey]) {
221
+ return <p>n/a</p>;
222
+ }
223
+
224
+ return (
225
+ <table className="mt-3 min-w-full divide-y divide-gray-300 border border-gray-200">
226
+ <thead className="bg-gray-50">
227
+ <tr>
228
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">
229
+ Percentage
230
+ </th>
231
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">Segments</th>
232
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">
233
+ Variables
234
+ </th>
235
+ </tr>
236
+ </thead>
237
+
238
+ <tbody>
239
+ {feature.environments[environmentKey].rules.map((rule, index) => {
240
+ return (
241
+ <tr key={index} className={index % 2 === 0 ? undefined : "bg-gray-50"}>
242
+ <td className="py-4 pl-4 pr-3 text-sm text-gray-900">{rule.percentage}%</td>
243
+ <td className="py-4 pl-4 pr-3 text-sm text-gray-900">
244
+ <ExpandRuleSegments segments={rule.segments} />
245
+ </td>
246
+ <td className="py-4 pl-4 pr-3 text-sm text-gray-900">
247
+ {rule.variables && (
248
+ <ul className="list-inside list-disc">
249
+ {Object.keys(rule.variables).map((k) => {
250
+ return (
251
+ <li key={k}>
252
+ <span className="font-semibold">{k}</span>:{" "}
253
+ {typeof rule.variables[k] === "string" && rule.variables[k]}
254
+ {typeof rule.variables[k] !== "string" && (
255
+ <code className="rounded bg-gray-100 px-2 py-1 text-red-400">
256
+ {rule.variables[k]}
257
+ </code>
258
+ )}
259
+ </li>
260
+ );
261
+ })}
262
+ </ul>
263
+ )}
264
+ </td>
265
+ </tr>
266
+ );
267
+ })}
268
+ </tbody>
269
+ </table>
270
+ );
271
+ }
272
+
273
+ export function DisplayFeatureRules() {
274
+ const { feature } = useOutletContext() as any;
275
+ const environmentKeys = Object.keys(feature.environments).sort();
276
+
277
+ const environmentTabs = environmentKeys.map((environmentKey, index) => {
278
+ return {
279
+ title: environmentKey,
280
+ to: `/features/${feature.key}/rules/${environmentKey}`,
281
+ };
282
+ });
283
+
284
+ return (
285
+ <>
286
+ <nav className="flex space-x-4" aria-label="Tabs">
287
+ {environmentTabs.map((tab) => (
288
+ <NavLink
289
+ key={tab.title}
290
+ to={tab.to}
291
+ className={({ isActive }) =>
292
+ [
293
+ isActive ? "bg-gray-200 text-gray-800" : "text-gray-600 hover:text-gray-800",
294
+ "rounded-md px-3 py-2 text-sm font-medium",
295
+ ].join(" ")
296
+ }
297
+ >
298
+ {tab.title}
299
+ </NavLink>
300
+ ))}
301
+ </nav>
302
+
303
+ <Outlet context={{ feature }} />
304
+ </>
305
+ );
306
+ }
307
+
308
+ export function DisplayFeatureVariations() {
309
+ const { feature } = useOutletContext() as any;
310
+
311
+ return (
312
+ <table className="min-w-full divide-y divide-gray-300 border border-gray-200">
313
+ <thead className="bg-gray-50">
314
+ <tr>
315
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">Value</th>
316
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">Weight</th>
317
+ {feature.variablesSchema && (
318
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">
319
+ Variables
320
+ </th>
321
+ )}
322
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">
323
+ Description
324
+ </th>
325
+ </tr>
326
+ </thead>
327
+
328
+ <tbody>
329
+ {feature.variations.map((variation, index) => {
330
+ return (
331
+ <tr key={variation.value} className={index % 2 === 0 ? undefined : "bg-gray-50"}>
332
+ <td className="py-4 pl-4 pr-3 text-sm font-medium text-gray-700 ">
333
+ {variation.type === "string" ? (
334
+ variation.value
335
+ ) : (
336
+ <pre>
337
+ <code className="rounded bg-gray-100 px-2 py-1 text-red-400">
338
+ {JSON.stringify(variation.value)}
339
+ </code>
340
+ </pre>
341
+ )}
342
+ </td>
343
+ <td className="py-4 pl-4 pr-3 text-sm font-medium text-gray-700">
344
+ {variation.weight}%
345
+ </td>
346
+
347
+ {feature.variablesSchema && (
348
+ <th className="py-4 pl-4 pr-3 text-sm font-medium text-gray-700">
349
+ {variation.variables && (
350
+ <ul className="list-inside list-disc text-left">
351
+ {variation.variables.map((v) => {
352
+ return (
353
+ <li key={v.key}>
354
+ <span className="font-semibold">{v.key}</span>:{" "}
355
+ {typeof v.value === "string" ? (
356
+ v.value
357
+ ) : (
358
+ <pre className="rounded bg-gray-100 px-2 py-1 text-red-400">
359
+ <code>{JSON.stringify(v.value, null, 2)}</code>
360
+ </pre>
361
+ )}
362
+ </li>
363
+ );
364
+ })}
365
+ </ul>
366
+ )}
367
+ </th>
368
+ )}
369
+ <td className="py-4 pl-4 pr-3 text-sm font-medium text-gray-700">
370
+ {variation.description || <span>n/a</span>}
371
+ </td>
372
+ </tr>
373
+ );
374
+ })}
375
+ </tbody>
376
+ </table>
377
+ );
378
+ }
379
+
380
+ export function DisplayFeatureVariablesSchema() {
381
+ const { feature } = useOutletContext() as any;
382
+
383
+ if (!feature.variablesSchema || feature.variablesSchema.length === 0) {
384
+ return <p>n/a</p>;
385
+ }
386
+
387
+ return (
388
+ <table className="min-w-full divide-y divide-gray-300 border border-gray-200">
389
+ <thead className="bg-gray-50">
390
+ <tr>
391
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">Key</th>
392
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">Type</th>
393
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">Default</th>
394
+ <th className="py-4 pl-4 pr-3 text-left text-sm font-semibold text-gray-500">
395
+ Description
396
+ </th>
397
+ </tr>
398
+ </thead>
399
+
400
+ <tbody>
401
+ {feature.variablesSchema.map((variableSchema, index) => {
402
+ return (
403
+ <tr key={variableSchema.key} className={index % 2 === 0 ? undefined : "bg-gray-50"}>
404
+ <td className="py-4 pl-4 pr-3 text-sm font-medium text-gray-700">
405
+ {variableSchema.key}
406
+ </td>
407
+ <td className="py-4 pl-4 pr-3 text-sm font-medium text-gray-700">
408
+ {variableSchema.type}
409
+ </td>
410
+ <td className="py-4 pl-4 pr-3 text-sm font-medium text-gray-700">
411
+ {variableSchema.type === "string" ? (
412
+ variableSchema.defaultValue
413
+ ) : (
414
+ <pre>
415
+ <code className="rounded bg-gray-100 px-2 py-1 text-red-400">
416
+ {JSON.stringify(variableSchema.defaultValue, null, 2)}
417
+ </code>
418
+ </pre>
419
+ )}
420
+ </td>
421
+ <td className="py-4 pl-4 pr-3 text-sm font-medium text-gray-700">
422
+ {variableSchema.description || <span>n/a</span>}
423
+ </td>
424
+ </tr>
425
+ );
426
+ })}
427
+ </tbody>
428
+ </table>
429
+ );
430
+ }
431
+
432
+ export function DisplayFeatureHistory() {
433
+ const { feature } = useOutletContext() as any;
434
+
435
+ return <HistoryTimeline entityType="feature" entityKey={feature.key} />;
436
+ }
437
+
438
+ export function ShowFeature(props) {
439
+ const { featureKey } = useParams();
440
+ const { data } = useSearchIndex();
441
+ const feature = data?.entities.features.find((f) => f.key === featureKey);
442
+ const links = data?.links;
443
+
444
+ if (!feature) {
445
+ return <p>Feature not found</p>;
446
+ }
447
+
448
+ const tabs = [
449
+ {
450
+ title: "Overview",
451
+ to: `/features/${featureKey}`,
452
+ },
453
+ ,
454
+ {
455
+ title: "Variations",
456
+ to: `/features/${featureKey}/variations`,
457
+ },
458
+ {
459
+ title: "Variables",
460
+ to: `/features/${featureKey}/variables`,
461
+ },
462
+ {
463
+ title: "Rules",
464
+ to: `/features/${featureKey}/rules`,
465
+ },
466
+ {
467
+ title: "Force",
468
+ to: `/features/${featureKey}/force`,
469
+ },
470
+ {
471
+ title: "History",
472
+ to: `/features/${featureKey}/history`,
473
+ },
474
+ ];
475
+
476
+ return (
477
+ <PageContent>
478
+ <PageTitle className="relative border-none">
479
+ Feature: {featureKey}{" "}
480
+ {links && <EditLink url={links.feature.replace("{{key}}", feature.key)} />}
481
+ </PageTitle>
482
+
483
+ <Tabs tabs={tabs} />
484
+
485
+ <div className="p-8">
486
+ <Outlet context={{ feature }} />
487
+ </div>
488
+ </PageContent>
489
+ );
490
+ }