@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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/index.css +2183 -0
- package/dist/index.js +3 -0
- package/dist/index.js.LICENSE.txt +68 -0
- package/dist/index.js.map +1 -0
- package/lib/components/Alert.d.ts +7 -0
- package/lib/components/App.d.ts +2 -0
- package/lib/components/EditLink.d.ts +2 -0
- package/lib/components/EnvironmentDot.d.ts +8 -0
- package/lib/components/ExpandConditions.d.ts +6 -0
- package/lib/components/ExpandRuleSegments.d.ts +6 -0
- package/lib/components/Footer.d.ts +2 -0
- package/lib/components/Header.d.ts +2 -0
- package/lib/components/HistoryTimeline.d.ts +10 -0
- package/lib/components/LastModified.d.ts +2 -0
- package/lib/components/ListAttributes.d.ts +2 -0
- package/lib/components/ListFeatures.d.ts +2 -0
- package/lib/components/ListHistory.d.ts +2 -0
- package/lib/components/ListSegments.d.ts +2 -0
- package/lib/components/Markdown.d.ts +2 -0
- package/lib/components/PageContent.d.ts +4 -0
- package/lib/components/PageTitle.d.ts +5 -0
- package/lib/components/PrettyDate.d.ts +7 -0
- package/lib/components/SearchInput.d.ts +7 -0
- package/lib/components/ShowAttribute.d.ts +5 -0
- package/lib/components/ShowFeature.d.ts +10 -0
- package/lib/components/ShowSegment.d.ts +5 -0
- package/lib/components/Tabs.d.ts +10 -0
- package/lib/components/Tag.d.ts +6 -0
- package/lib/contexts/SearchIndexContext.d.ts +7 -0
- package/lib/hooks/searchIndexHook.d.ts +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/utils/index.d.ts +23 -0
- package/package.json +62 -0
- package/public/favicon-128.png +0 -0
- package/public/index.html +15 -0
- package/src/components/Alert.tsx +22 -0
- package/src/components/App.tsx +129 -0
- package/src/components/EditLink.tsx +18 -0
- package/src/components/EnvironmentDot.tsx +48 -0
- package/src/components/ExpandConditions.tsx +84 -0
- package/src/components/ExpandRuleSegments.tsx +59 -0
- package/src/components/Footer.tsx +14 -0
- package/src/components/Header.tsx +62 -0
- package/src/components/HistoryTimeline.tsx +179 -0
- package/src/components/LastModified.tsx +29 -0
- package/src/components/ListAttributes.tsx +87 -0
- package/src/components/ListFeatures.tsx +95 -0
- package/src/components/ListHistory.tsx +17 -0
- package/src/components/ListSegments.tsx +81 -0
- package/src/components/Markdown.tsx +11 -0
- package/src/components/PageContent.tsx +9 -0
- package/src/components/PageTitle.tsx +23 -0
- package/src/components/PrettyDate.tsx +56 -0
- package/src/components/SearchInput.tsx +34 -0
- package/src/components/ShowAttribute.tsx +145 -0
- package/src/components/ShowFeature.tsx +490 -0
- package/src/components/ShowSegment.tsx +125 -0
- package/src/components/Tabs.tsx +46 -0
- package/src/components/Tag.tsx +13 -0
- package/src/contexts/SearchIndexContext.tsx +13 -0
- package/src/hooks/searchIndexHook.ts +9 -0
- package/src/index.css +8 -0
- package/src/index.tsx +12 -0
- package/src/utils/index.ts +203 -0
- package/tailwind.config.js +8 -0
- package/tsconfig.cjs.json +7 -0
- 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
|
+
}
|