@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,125 @@
1
+ import * as React from "react";
2
+ import { useParams, useOutletContext, Outlet, Link } 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 { ExpandConditions } from "./ExpandConditions";
9
+ import { EditLink } from "./EditLink";
10
+ import { useSearchIndex } from "../hooks/searchIndexHook";
11
+ import { Markdown } from "./Markdown";
12
+
13
+ export function DisplaySegmentOverview() {
14
+ const { segment } = useOutletContext() as any;
15
+
16
+ return (
17
+ <div className="border-gray-200">
18
+ <dl className="grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2">
19
+ <div>
20
+ <dt className="text-sm font-medium text-gray-500">Key</dt>
21
+ <dd className="mt-1 text-sm text-gray-900">{segment.key}</dd>
22
+ </div>
23
+ <div>
24
+ <dt className="text-sm font-medium text-gray-500">Archived</dt>
25
+ <dd className="mt-1 text-sm text-gray-900">
26
+ {segment.archived === true ? <span>Yes</span> : <span>No</span>}
27
+ </dd>
28
+ </div>
29
+
30
+ <div className="col-span-2">
31
+ <dt className="text-sm font-medium text-gray-500">Description</dt>
32
+ <dd className="mt-1 text-sm text-gray-900">
33
+ {segment.description.trim().length > 0 ? (
34
+ <Markdown children={segment.description} />
35
+ ) : (
36
+ "n/a"
37
+ )}
38
+ </dd>
39
+ </div>
40
+ <div className="col-span-2">
41
+ <dt className="text-sm font-medium text-gray-500">Conditions</dt>
42
+ <dd className="mt-1 text-sm text-gray-900">
43
+ <ExpandConditions conditions={segment.conditions} />
44
+ </dd>
45
+ </div>
46
+ </dl>
47
+ </div>
48
+ );
49
+ }
50
+
51
+ export function DisplaySegmentUsage() {
52
+ const { segment } = useOutletContext() as any;
53
+
54
+ return (
55
+ <div className="border-gray-200">
56
+ <dl className="grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2">
57
+ <div>
58
+ <dt className="text-sm font-medium text-gray-500">Features</dt>
59
+ <dd className="mt-1 text-sm text-gray-900">
60
+ {segment.usedInFeatures.length === 0 && "none"}
61
+
62
+ {segment.usedInFeatures.length > 0 && (
63
+ <ul className="list-inside list-disc">
64
+ {segment.usedInFeatures.map((feature) => {
65
+ return (
66
+ <li key={feature}>
67
+ <Link to={`/features/${feature}`}>{feature}</Link>
68
+ </li>
69
+ );
70
+ })}
71
+ </ul>
72
+ )}
73
+ </dd>
74
+ </div>
75
+ </dl>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ export function DisplaySegmentHistory() {
81
+ const { segment } = useOutletContext() as any;
82
+
83
+ return <HistoryTimeline entityType="segment" entityKey={segment.key} />;
84
+ }
85
+
86
+ export function ShowSegment(props) {
87
+ const { segmentKey } = useParams();
88
+ const { data } = useSearchIndex();
89
+ const links = data?.links;
90
+ const segment = data?.entities.segments.find((s) => s.key === segmentKey);
91
+
92
+ if (!segment) {
93
+ return <div>Segment not found</div>;
94
+ }
95
+
96
+ const tabs = [
97
+ {
98
+ title: "Overview",
99
+ to: `/segments/${segmentKey}`,
100
+ },
101
+ {
102
+ title: "Usage",
103
+ to: `/segments/${segmentKey}/usage`,
104
+ },
105
+ {
106
+ title: "History",
107
+ to: `/segments/${segmentKey}/history`,
108
+ },
109
+ ];
110
+
111
+ return (
112
+ <PageContent>
113
+ <PageTitle className="border-none">
114
+ Segment: {segmentKey}{" "}
115
+ {links && <EditLink url={links.segment.replace("{{key}}", segment.key)} />}
116
+ </PageTitle>
117
+
118
+ <Tabs tabs={tabs} />
119
+
120
+ <div className="p-8">
121
+ <Outlet context={{ segment }} />
122
+ </div>
123
+ </PageContent>
124
+ );
125
+ }
@@ -0,0 +1,46 @@
1
+ import * as React from "react";
2
+ import { NavLink } from "react-router-dom";
3
+
4
+ interface Tab {
5
+ title: string;
6
+ to: string;
7
+ }
8
+
9
+ interface TabsProps {
10
+ tabs: Tab[];
11
+ }
12
+
13
+ export function Tabs(props: TabsProps) {
14
+ const { tabs } = props;
15
+
16
+ return (
17
+ <div className="border-b border-gray-200">
18
+ <div className="flex">
19
+ {tabs.map((tab) => (
20
+ <NavLink
21
+ key={tab.title}
22
+ to={tab.to}
23
+ end
24
+ className={({ isActive }) =>
25
+ [
26
+ "w-1/4",
27
+ "border-b-2",
28
+ "pt-2",
29
+ "pb-4",
30
+ "px-1",
31
+ "text-center",
32
+ "text-sm",
33
+ "font-medium",
34
+ isActive
35
+ ? "border-slate-500 text-slate-600"
36
+ : "border-transparent text-slate-500 hover:border-slate-300 hover:text-slate-700",
37
+ ].join(" ")
38
+ }
39
+ >
40
+ {tab.title}
41
+ </NavLink>
42
+ ))}
43
+ </div>
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+
3
+ interface TagProps {
4
+ tag: string;
5
+ }
6
+
7
+ export function Tag(props: TagProps) {
8
+ return (
9
+ <span className="mr-1 rounded-full border-slate-400 bg-slate-300 px-2 py-1 text-xs text-gray-600">
10
+ {props.tag}
11
+ </span>
12
+ );
13
+ }
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+
3
+ import { SearchIndex } from "@featurevisor/types";
4
+
5
+ export interface SearchIndexProps {
6
+ isLoaded: boolean;
7
+ data?: SearchIndex;
8
+ }
9
+
10
+ export const SearchIndexContext = React.createContext<SearchIndexProps>({
11
+ isLoaded: false,
12
+ data: undefined,
13
+ });
@@ -0,0 +1,9 @@
1
+ import { useContext } from "react";
2
+
3
+ import { SearchIndexContext } from "../contexts/SearchIndexContext";
4
+
5
+ export function useSearchIndex() {
6
+ const value = useContext(SearchIndexContext);
7
+
8
+ return value;
9
+ }
package/src/index.css ADDED
@@ -0,0 +1,8 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ html,
6
+ body {
7
+ background: theme("colors.gray.200");
8
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,12 @@
1
+ import * as React from "react";
2
+ import * as ReactDOM from "react-dom";
3
+ import { HashRouter } from "react-router-dom";
4
+
5
+ import { App } from "./components/App";
6
+
7
+ ReactDOM.render(
8
+ <HashRouter>
9
+ <App />
10
+ </HashRouter>,
11
+ document.getElementById("root"),
12
+ );
@@ -0,0 +1,203 @@
1
+ import { SearchIndex } from "@featurevisor/types";
2
+
3
+ export interface Query {
4
+ keyword: string;
5
+ tags: string[];
6
+ environments: string[];
7
+ archived?: boolean;
8
+ capture?: boolean;
9
+ }
10
+
11
+ export function getQueryFromString(q: string) {
12
+ const query: Query = {
13
+ keyword: "",
14
+ tags: [],
15
+ environments: [],
16
+ archived: undefined,
17
+ capture: undefined,
18
+ };
19
+
20
+ const parts = q.split(" ");
21
+
22
+ for (const part of parts) {
23
+ if (part.startsWith("tag:")) {
24
+ const tag = part.replace("tag:", "");
25
+
26
+ if (tag.length > 0) {
27
+ query.tags.push(tag);
28
+ }
29
+ } else if (part.startsWith("in:")) {
30
+ const environment = part.replace("in:", "");
31
+
32
+ if (environment.length > 0) {
33
+ query.environments.push(environment);
34
+ }
35
+ } else if (part.startsWith("archived:")) {
36
+ const archived = part.replace("archived:", "");
37
+
38
+ if (archived === "true") {
39
+ query.archived = true;
40
+ } else if (archived === "false") {
41
+ query.archived = false;
42
+ }
43
+ } else if (part.startsWith("capture:")) {
44
+ const capture = part.replace("capture:", "");
45
+
46
+ if (capture === "true") {
47
+ query.capture = true;
48
+ } else if (capture === "false") {
49
+ query.capture = false;
50
+ }
51
+ } else {
52
+ if (part.length > 0) {
53
+ query.keyword = part;
54
+ }
55
+ }
56
+ }
57
+
58
+ return query;
59
+ }
60
+
61
+ export function isEnabledInEnvironment(feature: any, environment: string) {
62
+ if (feature.archived === true) {
63
+ return false;
64
+ }
65
+
66
+ if (!feature.environments[environment]) {
67
+ return false;
68
+ }
69
+
70
+ if (feature.environments[environment].exposed === false) {
71
+ return false;
72
+ }
73
+
74
+ if (feature.environments[environment].rules.some((rule: any) => rule.percentage > 0)) {
75
+ return true;
76
+ }
77
+
78
+ return false;
79
+ }
80
+
81
+ export function isEnabledInAnyEnvironment(feature: any) {
82
+ const environments = Object.keys(feature.environments);
83
+
84
+ for (const environment of environments) {
85
+ const isEnabled = isEnabledInEnvironment(feature, environment);
86
+
87
+ if (isEnabled) {
88
+ return true;
89
+ }
90
+ }
91
+
92
+ return false;
93
+ }
94
+
95
+ export function getFeaturesByQuery(query: Query, data: SearchIndex) {
96
+ const features = data.entities.features
97
+ .filter((feature) => {
98
+ let matched = true;
99
+
100
+ if (query.keyword.length > 0 && feature.key.indexOf(query.keyword.toLowerCase()) === -1) {
101
+ matched = false;
102
+ }
103
+
104
+ if (query.tags.length > 0) {
105
+ for (const tag of query.tags) {
106
+ if (feature.tags.every((t: string) => t.toLowerCase() !== tag.toLowerCase())) {
107
+ matched = false;
108
+ }
109
+ }
110
+ }
111
+
112
+ if (query.environments.length > 0 && feature.archived !== false) {
113
+ for (const environment of query.environments) {
114
+ if (isEnabledInEnvironment(feature, environment) === false) {
115
+ matched = false;
116
+ }
117
+ }
118
+ }
119
+
120
+ if (typeof query.archived === "boolean") {
121
+ if (query.archived && feature.archived !== query.archived) {
122
+ matched = false;
123
+ }
124
+
125
+ if (!query.archived && feature.archived === true) {
126
+ matched = false;
127
+ }
128
+ }
129
+
130
+ return matched;
131
+ })
132
+ .sort((a, b) => a.key.localeCompare(b.key));
133
+
134
+ return features;
135
+ }
136
+
137
+ export function getAttributesByQuery(query: Query, data: SearchIndex) {
138
+ const attributes = data.entities.attributes
139
+ .filter((a) => {
140
+ let matched = true;
141
+
142
+ if (
143
+ query.keyword.length > 0 &&
144
+ a.key.toLowerCase().indexOf(query.keyword.toLowerCase()) === -1
145
+ ) {
146
+ matched = false;
147
+ }
148
+
149
+ if (typeof query.archived === "boolean") {
150
+ if (query.archived && a.archived !== query.archived) {
151
+ matched = false;
152
+ }
153
+
154
+ if (!query.archived && a.archived === true) {
155
+ matched = false;
156
+ }
157
+ }
158
+
159
+ if (typeof query.capture === "boolean") {
160
+ if (query.capture && a.capture !== query.capture) {
161
+ matched = false;
162
+ }
163
+
164
+ if (!query.capture && a.capture === true) {
165
+ matched = false;
166
+ }
167
+ }
168
+
169
+ return matched;
170
+ })
171
+ .sort((a, b) => a.key.localeCompare(b.key));
172
+
173
+ return attributes;
174
+ }
175
+
176
+ export function getSegmentsByQuery(query: Query, data: SearchIndex) {
177
+ const segments = data.entities.segments
178
+ .filter((a) => {
179
+ let matched = true;
180
+
181
+ if (
182
+ query.keyword.length > 0 &&
183
+ a.key.toLowerCase().indexOf(query.keyword.toLowerCase()) === -1
184
+ ) {
185
+ matched = false;
186
+ }
187
+
188
+ if (typeof query.archived === "boolean") {
189
+ if (query.archived && a.archived !== query.archived) {
190
+ matched = false;
191
+ }
192
+
193
+ if (!query.archived && a.archived === true) {
194
+ matched = false;
195
+ }
196
+ }
197
+
198
+ return matched;
199
+ })
200
+ .sort((a, b) => a.key.localeCompare(b.key));
201
+
202
+ return segments;
203
+ }
@@ -0,0 +1,8 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ content: ["./src/**/*.html", "./src/**/*.tsx"],
4
+ theme: {
5
+ extend: {},
6
+ },
7
+ plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")],
8
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.cjs.json",
3
+ "compilerOptions": {
4
+ "outDir": "./lib"
5
+ },
6
+ "include": ["./src/**/*.ts", "./src/**/*.tsx"]
7
+ }
@@ -0,0 +1,14 @@
1
+ const path = require("path");
2
+
3
+ const getWebpackConfig = require("../../tools/getWebpackConfig");
4
+
5
+ const wepbackConfig = getWebpackConfig({
6
+ entryFilePath: path.join(__dirname, "src", "index.tsx"),
7
+ entryKey: "index",
8
+ outputDirectoryPath: path.join(__dirname, "dist"),
9
+ outputLibrary: "FeaturevisorSite",
10
+ tsConfigFilePath: path.join(__dirname, "tsconfig.cjs.json"),
11
+ enableCssModules: true,
12
+ });
13
+
14
+ module.exports = wepbackConfig;