@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,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export function Footer() {
|
|
4
|
+
return (
|
|
5
|
+
<footer>
|
|
6
|
+
<p className="pb-5 text-center text-xs leading-5 text-gray-500">
|
|
7
|
+
Site built using{" "}
|
|
8
|
+
<a target="_blank" href="https://featurevisor.com">
|
|
9
|
+
Featurevisor
|
|
10
|
+
</a>
|
|
11
|
+
</p>
|
|
12
|
+
</footer>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Link, NavLink } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
export function Header() {
|
|
5
|
+
const navItems = [
|
|
6
|
+
{
|
|
7
|
+
title: "Features",
|
|
8
|
+
to: "features",
|
|
9
|
+
active: true,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
title: "Segments",
|
|
13
|
+
to: "segments",
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
title: "Attributes",
|
|
17
|
+
to: "attributes",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
title: "History",
|
|
21
|
+
to: "history",
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="bg-gray-800">
|
|
27
|
+
<nav className="mx-auto flex max-w-4xl items-center justify-between px-8 pb-4 pt-3">
|
|
28
|
+
<Link to="/" className="text-gray-50">
|
|
29
|
+
<img
|
|
30
|
+
alt="Featurevisor"
|
|
31
|
+
src="/favicon-128.png"
|
|
32
|
+
className="absolute top-4 -ml-2 w-[36px]"
|
|
33
|
+
/>
|
|
34
|
+
</Link>
|
|
35
|
+
|
|
36
|
+
<div className="relative flex gap-x-4">
|
|
37
|
+
{navItems.map((item) => (
|
|
38
|
+
<NavLink
|
|
39
|
+
key={item.title}
|
|
40
|
+
to={item.to}
|
|
41
|
+
className={({ isActive }) => {
|
|
42
|
+
return [
|
|
43
|
+
"relative",
|
|
44
|
+
"rounded-lg",
|
|
45
|
+
isActive ? "bg-gray-700" : "",
|
|
46
|
+
"px-3",
|
|
47
|
+
"py-2",
|
|
48
|
+
"text-sm",
|
|
49
|
+
"font-semibold",
|
|
50
|
+
"leading-6",
|
|
51
|
+
"text-gray-50",
|
|
52
|
+
].join(" ");
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
{item.title}
|
|
56
|
+
</NavLink>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
</nav>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { UserCircleIcon } from "@heroicons/react/20/solid";
|
|
4
|
+
|
|
5
|
+
import { Alert } from "./Alert";
|
|
6
|
+
import { PrettyDate } from "./PrettyDate";
|
|
7
|
+
import { useSearchIndex } from "../hooks/searchIndexHook";
|
|
8
|
+
|
|
9
|
+
const entriesPerPage = 50;
|
|
10
|
+
const initialMaxEntitiesCount = 10;
|
|
11
|
+
|
|
12
|
+
function Activity(props) {
|
|
13
|
+
const { entry, links } = props;
|
|
14
|
+
const [showAll, setShowAll] = React.useState(false);
|
|
15
|
+
|
|
16
|
+
const entitiesToRender = showAll
|
|
17
|
+
? entry.entities
|
|
18
|
+
: entry.entities.slice(0, initialMaxEntitiesCount);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
<span className="font-semibold text-gray-600">{entry.author}</span>{" "}
|
|
23
|
+
updated{" "}
|
|
24
|
+
{entry.entities.length === 1 ? (
|
|
25
|
+
<>
|
|
26
|
+
{entry.entities[0].type}{" "}
|
|
27
|
+
<a href="#" className="font-semibold text-gray-600">
|
|
28
|
+
{entry.entities[0].key}
|
|
29
|
+
</a>
|
|
30
|
+
</>
|
|
31
|
+
) : (
|
|
32
|
+
""
|
|
33
|
+
)}{" "}
|
|
34
|
+
on{" "}
|
|
35
|
+
<a
|
|
36
|
+
href={
|
|
37
|
+
links
|
|
38
|
+
? links.commit.replace("{{hash}}", entry.commit)
|
|
39
|
+
: `#${entry.commit}`
|
|
40
|
+
}
|
|
41
|
+
target="_blank"
|
|
42
|
+
className="font-semibold text-gray-600"
|
|
43
|
+
>
|
|
44
|
+
<PrettyDate date={entry.timestamp} showTime={props.showTime} />
|
|
45
|
+
</a>
|
|
46
|
+
{entry.entities.length > 1 && (
|
|
47
|
+
<>
|
|
48
|
+
<span>:</span>
|
|
49
|
+
<ul className="list-disc pl-3.5 pt-2 text-gray-400 marker:text-gray-400">
|
|
50
|
+
{entitiesToRender.map((entity, index) => {
|
|
51
|
+
return (
|
|
52
|
+
<li key={index}>
|
|
53
|
+
<span className="text-gray-400">{entity.type}</span>{" "}
|
|
54
|
+
<a href="#" className="font-semibold text-gray-500">
|
|
55
|
+
{entity.key}
|
|
56
|
+
</a>
|
|
57
|
+
</li>
|
|
58
|
+
);
|
|
59
|
+
})}
|
|
60
|
+
|
|
61
|
+
{!showAll && entry.entities.length > initialMaxEntitiesCount && (
|
|
62
|
+
<li key="show-all">
|
|
63
|
+
<a
|
|
64
|
+
href="#"
|
|
65
|
+
className="font-bold underline"
|
|
66
|
+
onClick={() => setShowAll(true)}
|
|
67
|
+
>
|
|
68
|
+
Show all
|
|
69
|
+
</a>
|
|
70
|
+
</li>
|
|
71
|
+
)}
|
|
72
|
+
</ul>
|
|
73
|
+
</>
|
|
74
|
+
)}
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface HistoryTimelineProps {
|
|
80
|
+
className?: string;
|
|
81
|
+
filter?: (entry: any) => boolean;
|
|
82
|
+
entityType?: string;
|
|
83
|
+
entityKey?: string;
|
|
84
|
+
showTime?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function HistoryTimeline(props: HistoryTimelineProps) {
|
|
88
|
+
const { data } = useSearchIndex();
|
|
89
|
+
const links = data?.links;
|
|
90
|
+
const [historyEntries, setHistoryEntries] = React.useState([]);
|
|
91
|
+
|
|
92
|
+
const [page, setPage] = React.useState(1);
|
|
93
|
+
|
|
94
|
+
React.useEffect(() => {
|
|
95
|
+
fetch("/history-full.json")
|
|
96
|
+
.then((res) => res.json())
|
|
97
|
+
.then((data) => {
|
|
98
|
+
const filteredHistoryEntries = data.filter((entry: any) => {
|
|
99
|
+
if (props.filter) {
|
|
100
|
+
return props.filter(entry);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (props.entityType && props.entityKey) {
|
|
104
|
+
return entry.entities.some(
|
|
105
|
+
(entity: any) =>
|
|
106
|
+
entity.type === props.entityType &&
|
|
107
|
+
entity.key === props.entityKey
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return true;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
setHistoryEntries(filteredHistoryEntries);
|
|
115
|
+
});
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
const entriesToRender = historyEntries.slice(0, page * entriesPerPage);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className={props.className || ""}>
|
|
122
|
+
{entriesToRender.length === 0 && (
|
|
123
|
+
<Alert type="warning">No history found.</Alert>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{entriesToRender.length > 0 && (
|
|
127
|
+
<div>
|
|
128
|
+
<ul className="mt-8">
|
|
129
|
+
<li>
|
|
130
|
+
{entriesToRender
|
|
131
|
+
.slice(0, page * entriesPerPage)
|
|
132
|
+
.map((entry: any, index) => {
|
|
133
|
+
const isNotLast = entriesToRender.length !== index + 1;
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className="relative pb-8">
|
|
137
|
+
{isNotLast ? (
|
|
138
|
+
<span className="absolute left-4 top-5 -ml-px h-full w-0.5 bg-gray-200" />
|
|
139
|
+
) : (
|
|
140
|
+
""
|
|
141
|
+
)}
|
|
142
|
+
<div className="relative flex space-x-3">
|
|
143
|
+
<div>
|
|
144
|
+
<div className="relative">
|
|
145
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-100 ring-4 ring-white">
|
|
146
|
+
<UserCircleIcon className="h-5 w-5 text-gray-500" />
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
<div className="min-w-0 flex-1 py-1.5">
|
|
151
|
+
<div className="text-sm text-gray-500">
|
|
152
|
+
<Activity
|
|
153
|
+
entry={entry}
|
|
154
|
+
showTime={props.showTime}
|
|
155
|
+
links={links}
|
|
156
|
+
/>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
})}
|
|
163
|
+
</li>
|
|
164
|
+
</ul>
|
|
165
|
+
|
|
166
|
+
{historyEntries.length > entriesToRender.length && (
|
|
167
|
+
<a
|
|
168
|
+
href="#"
|
|
169
|
+
className="text-md block rounded-md border border-gray-300 bg-gray-50 py-2 pl-6 text-center font-bold text-gray-500 shadow-sm hover:bg-gray-100"
|
|
170
|
+
onClick={() => setPage(page + 1)}
|
|
171
|
+
>
|
|
172
|
+
Load more
|
|
173
|
+
</a>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { PrettyDate } from "./PrettyDate";
|
|
4
|
+
|
|
5
|
+
export function LastModified(props: any) {
|
|
6
|
+
const { lastModified } = props;
|
|
7
|
+
|
|
8
|
+
if (!lastModified) {
|
|
9
|
+
return (
|
|
10
|
+
<p className="inline-flex rounded-full px-2 text-xs leading-5 text-gray-500">
|
|
11
|
+
Last modified
|
|
12
|
+
<span className="pl-1 font-semibold text-gray-600">n/a</span>
|
|
13
|
+
</p>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<p className="inline-flex rounded-full px-2 text-xs leading-5 text-gray-500">
|
|
19
|
+
Last modified by
|
|
20
|
+
<span className="pl-1 pr-1 font-semibold text-gray-600">
|
|
21
|
+
{lastModified.author}
|
|
22
|
+
</span>{" "}
|
|
23
|
+
on
|
|
24
|
+
<span className="pl-1 font-semibold text-gray-600">
|
|
25
|
+
<PrettyDate date={lastModified.timestamp} />
|
|
26
|
+
</span>
|
|
27
|
+
</p>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import { SearchIndex } from "@featurevisor/types";
|
|
5
|
+
import { useSearchIndex } from "../hooks/searchIndexHook";
|
|
6
|
+
import { getQueryFromString, getAttributesByQuery } from "../utils";
|
|
7
|
+
import { Tag } from "./Tag";
|
|
8
|
+
import { Alert } from "./Alert";
|
|
9
|
+
import { SearchInput } from "./SearchInput";
|
|
10
|
+
import { PageTitle } from "./PageTitle";
|
|
11
|
+
import { PageContent } from "./PageContent";
|
|
12
|
+
import { LastModified } from "./LastModified";
|
|
13
|
+
|
|
14
|
+
export function ListAttributes() {
|
|
15
|
+
const [q, setQ] = React.useState("");
|
|
16
|
+
|
|
17
|
+
const contextValue = useSearchIndex();
|
|
18
|
+
const data = contextValue.data as SearchIndex;
|
|
19
|
+
|
|
20
|
+
const query = getQueryFromString(q);
|
|
21
|
+
const attributes = getAttributesByQuery(query, data);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<PageContent>
|
|
25
|
+
<PageTitle>Attributes</PageTitle>
|
|
26
|
+
|
|
27
|
+
<SearchInput value={q} onChange={(e: any) => setQ(e.target.value)} />
|
|
28
|
+
|
|
29
|
+
{attributes.length === 0 && <Alert type="warning">No results found</Alert>}
|
|
30
|
+
|
|
31
|
+
{attributes.length > 0 && (
|
|
32
|
+
<div>
|
|
33
|
+
<ul className="diving-gray-200 divide-y">
|
|
34
|
+
{attributes.map((attribute: any) => (
|
|
35
|
+
<li key={attribute.key}>
|
|
36
|
+
<Link to={`/attributes/${attribute.key}`} className="block hover:bg-gray-50">
|
|
37
|
+
<div className="px-6 py-4">
|
|
38
|
+
<div className="flex items-center justify-between">
|
|
39
|
+
<p className="text-md relative font-bold text-slate-600">
|
|
40
|
+
{attribute.key}{" "}
|
|
41
|
+
{attribute.capture && (
|
|
42
|
+
<span className="ml-1 rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800">
|
|
43
|
+
capture
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
46
|
+
{attribute.archived && (
|
|
47
|
+
<span className="ml-1 rounded-full bg-red-100 px-2 py-1 text-xs font-medium text-red-800">
|
|
48
|
+
archived
|
|
49
|
+
</span>
|
|
50
|
+
)}
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
<div className="ml-2 flex flex-shrink-0 text-xs text-gray-500">
|
|
54
|
+
<div>
|
|
55
|
+
Used in: <Tag tag={`${attribute.usedInSegments.length} segments`} />{" "}
|
|
56
|
+
<Tag tag={`${attribute.usedInFeatures.length} features`} />
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<div className="mt-2 flex justify-between">
|
|
62
|
+
<div className="flex">
|
|
63
|
+
<p className="line-clamp-3 max-w-md items-center text-sm text-gray-500">
|
|
64
|
+
{attribute.description && attribute.description.trim().length > 0
|
|
65
|
+
? attribute.description
|
|
66
|
+
: "n/a"}
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className="items-top mt-2 flex text-xs text-gray-500 sm:mt-0">
|
|
71
|
+
<LastModified lastModified={attribute.lastModified} />
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</Link>
|
|
76
|
+
</li>
|
|
77
|
+
))}
|
|
78
|
+
</ul>
|
|
79
|
+
|
|
80
|
+
<p className="mt-6 text-center text-sm text-gray-500">
|
|
81
|
+
A total of <span className="font-bold">{attributes.length}</span> results found.
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</PageContent>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import { TagIcon } from "@heroicons/react/20/solid";
|
|
5
|
+
|
|
6
|
+
import { SearchIndex } from "@featurevisor/types";
|
|
7
|
+
import { useSearchIndex } from "../hooks/searchIndexHook";
|
|
8
|
+
import { getQueryFromString, getFeaturesByQuery } from "../utils";
|
|
9
|
+
import { EnvironmentDot } from "./EnvironmentDot";
|
|
10
|
+
import { Tag } from "./Tag";
|
|
11
|
+
import { Alert } from "./Alert";
|
|
12
|
+
import { SearchInput } from "./SearchInput";
|
|
13
|
+
import { PageTitle } from "./PageTitle";
|
|
14
|
+
import { PageContent } from "./PageContent";
|
|
15
|
+
import { LastModified } from "./LastModified";
|
|
16
|
+
|
|
17
|
+
export function ListFeatures() {
|
|
18
|
+
const [q, setQ] = React.useState("");
|
|
19
|
+
|
|
20
|
+
const contextValue = useSearchIndex();
|
|
21
|
+
const data = contextValue.data as SearchIndex;
|
|
22
|
+
|
|
23
|
+
const query = getQueryFromString(q);
|
|
24
|
+
const features = getFeaturesByQuery(query, data);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<PageContent>
|
|
28
|
+
<PageTitle>Features</PageTitle>
|
|
29
|
+
|
|
30
|
+
<SearchInput value={q} onChange={(e: any) => setQ(e.target.value)} />
|
|
31
|
+
|
|
32
|
+
{features.length === 0 && <Alert type="warning">No results found</Alert>}
|
|
33
|
+
|
|
34
|
+
{features.length > 0 && (
|
|
35
|
+
<div>
|
|
36
|
+
<ul className="diving-gray-200 divide-y">
|
|
37
|
+
{features.map((feature: any) => (
|
|
38
|
+
<li key={feature.key}>
|
|
39
|
+
<Link to={`/features/${feature.key}`}>
|
|
40
|
+
<div className="block hover:bg-gray-50">
|
|
41
|
+
<div className="px-6 py-4">
|
|
42
|
+
<div className="flex items-center justify-between">
|
|
43
|
+
<p className="text-md relative font-bold text-slate-600">
|
|
44
|
+
<EnvironmentDot
|
|
45
|
+
feature={feature}
|
|
46
|
+
className="relative top-[0.5px] inline-block pr-2"
|
|
47
|
+
/>{" "}
|
|
48
|
+
<a href="#" className="font-bold">
|
|
49
|
+
{feature.key}
|
|
50
|
+
</a>{" "}
|
|
51
|
+
{feature.archived && (
|
|
52
|
+
<span className="ml-1 rounded-full bg-red-100 px-2 py-1 text-xs font-medium text-red-800">
|
|
53
|
+
archived
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
<div className="ml-2 flex flex-shrink-0">
|
|
59
|
+
<div>
|
|
60
|
+
<TagIcon className="inline-block h-6 w-6 pr-1 text-xs text-gray-400" />
|
|
61
|
+
{feature.tags.map((tag: string) => (
|
|
62
|
+
<Tag tag={tag} key={tag} />
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div className="mt-2 flex justify-between">
|
|
69
|
+
<div className="flex">
|
|
70
|
+
<p className="line-clamp-3 max-w-md items-center pl-6 text-sm text-gray-500">
|
|
71
|
+
{feature.description && feature.description.trim().length > 0
|
|
72
|
+
? feature.description
|
|
73
|
+
: "n/a"}
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="items-top mt-2 flex text-xs text-gray-500 sm:mt-0">
|
|
78
|
+
<LastModified lastModified={feature.lastModified} />
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</Link>
|
|
84
|
+
</li>
|
|
85
|
+
))}
|
|
86
|
+
</ul>
|
|
87
|
+
|
|
88
|
+
<p className="mt-6 text-center text-sm text-gray-500">
|
|
89
|
+
A total of <span className="font-bold">{features.length}</span> results found.
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
)}
|
|
93
|
+
</PageContent>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { PageContent } from "./PageContent";
|
|
4
|
+
import { PageTitle } from "./PageTitle";
|
|
5
|
+
import { HistoryTimeline } from "./HistoryTimeline";
|
|
6
|
+
|
|
7
|
+
export function ListHistory() {
|
|
8
|
+
return (
|
|
9
|
+
<PageContent>
|
|
10
|
+
<PageTitle>History</PageTitle>
|
|
11
|
+
|
|
12
|
+
<div className="px-8">
|
|
13
|
+
<HistoryTimeline showTime />
|
|
14
|
+
</div>
|
|
15
|
+
</PageContent>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Link } from "react-router-dom";
|
|
3
|
+
|
|
4
|
+
import { SearchIndex } from "@featurevisor/types";
|
|
5
|
+
import { useSearchIndex } from "../hooks/searchIndexHook";
|
|
6
|
+
import { getQueryFromString, getSegmentsByQuery } from "../utils";
|
|
7
|
+
import { Tag } from "./Tag";
|
|
8
|
+
import { Alert } from "./Alert";
|
|
9
|
+
import { SearchInput } from "./SearchInput";
|
|
10
|
+
import { PageTitle } from "./PageTitle";
|
|
11
|
+
import { PageContent } from "./PageContent";
|
|
12
|
+
import { LastModified } from "./LastModified";
|
|
13
|
+
|
|
14
|
+
export function ListSegments() {
|
|
15
|
+
const [q, setQ] = React.useState("");
|
|
16
|
+
|
|
17
|
+
const contextValue = useSearchIndex();
|
|
18
|
+
const data = contextValue.data as SearchIndex;
|
|
19
|
+
|
|
20
|
+
const query = getQueryFromString(q);
|
|
21
|
+
const segments = getSegmentsByQuery(query, data);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<PageContent>
|
|
25
|
+
<PageTitle>Segments</PageTitle>
|
|
26
|
+
|
|
27
|
+
<SearchInput value={q} onChange={(e: any) => setQ(e.target.value)} />
|
|
28
|
+
|
|
29
|
+
{segments.length === 0 && <Alert type="warning">No results found</Alert>}
|
|
30
|
+
|
|
31
|
+
{segments.length > 0 && (
|
|
32
|
+
<div>
|
|
33
|
+
<ul className="diving-gray-200 divide-y">
|
|
34
|
+
{segments.map((segment: any) => (
|
|
35
|
+
<li key={segment.key}>
|
|
36
|
+
<Link to={`/segments/${segment.key}`} className="block hover:bg-gray-50">
|
|
37
|
+
<div className="px-6 py-4">
|
|
38
|
+
<div className="flex items-center justify-between">
|
|
39
|
+
<p className="text-md relative font-bold text-slate-600">
|
|
40
|
+
{segment.key}{" "}
|
|
41
|
+
{segment.archived && (
|
|
42
|
+
<span className="ml-1 rounded-full bg-red-100 px-2 py-1 text-xs font-medium text-red-800">
|
|
43
|
+
archived
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
<div className="ml-2 flex flex-shrink-0 text-xs text-gray-500">
|
|
49
|
+
<div>
|
|
50
|
+
Used in: <Tag tag={`${segment.usedInFeatures.length} features`} />
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="mt-2 flex justify-between">
|
|
56
|
+
<div className="flex">
|
|
57
|
+
<p className="line-clamp-3 max-w-md items-center text-sm text-gray-500">
|
|
58
|
+
{segment.description && segment.description.trim().length > 0
|
|
59
|
+
? segment.description
|
|
60
|
+
: "n/a"}
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="items-top mt-2 flex text-xs text-gray-500 sm:mt-0">
|
|
65
|
+
<LastModified lastModified={segment.lastModified} />
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</Link>
|
|
70
|
+
</li>
|
|
71
|
+
))}
|
|
72
|
+
</ul>
|
|
73
|
+
|
|
74
|
+
<p className="mt-6 text-center text-sm text-gray-500">
|
|
75
|
+
A total of <span className="font-bold">{segments.length}</span> results found.
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
)}
|
|
79
|
+
</PageContent>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
export function PageTitle(props: {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
}) {
|
|
7
|
+
return (
|
|
8
|
+
<h1
|
|
9
|
+
className={[
|
|
10
|
+
"mx-6",
|
|
11
|
+
"border-b",
|
|
12
|
+
"pb-4",
|
|
13
|
+
"pt-8",
|
|
14
|
+
"text-3xl",
|
|
15
|
+
"font-black",
|
|
16
|
+
"text-gray-700",
|
|
17
|
+
props.className ? props.className : "",
|
|
18
|
+
].join(" ")}
|
|
19
|
+
>
|
|
20
|
+
{props.children}
|
|
21
|
+
</h1>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
function getPrettyDate(props) {
|
|
4
|
+
const { showTime } = props;
|
|
5
|
+
const date = new Date(props.date);
|
|
6
|
+
|
|
7
|
+
const day = date.getDate();
|
|
8
|
+
const month = new Intl.DateTimeFormat("en-US", { month: "short" }).format(
|
|
9
|
+
date
|
|
10
|
+
);
|
|
11
|
+
const year = date.getFullYear();
|
|
12
|
+
|
|
13
|
+
const ordinalSuffix = (day: number) => {
|
|
14
|
+
switch (day % 10) {
|
|
15
|
+
case 1:
|
|
16
|
+
return "st";
|
|
17
|
+
case 2:
|
|
18
|
+
return "nd";
|
|
19
|
+
case 3:
|
|
20
|
+
return "rd";
|
|
21
|
+
default:
|
|
22
|
+
return "th";
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const prettyDate = `${day}${ordinalSuffix(day)} ${month} ${year}`;
|
|
27
|
+
|
|
28
|
+
if (showTime) {
|
|
29
|
+
const hours = date.getHours();
|
|
30
|
+
const minutes = date.getMinutes();
|
|
31
|
+
const ampm = hours >= 12 ? "PM" : "AM";
|
|
32
|
+
const hour12 = hours % 12 || 12;
|
|
33
|
+
const prettyTime = `${hour12}:${minutes
|
|
34
|
+
.toString()
|
|
35
|
+
.padStart(2, "0")}${ampm}`;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<span className="font-normal text-gray-500">{prettyDate}</span>{" "}
|
|
40
|
+
<span className="font-normal text-gray-500">at</span>{" "}
|
|
41
|
+
<span className="font-normal text-gray-500">{prettyTime}</span>
|
|
42
|
+
</>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return prettyDate;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface PrettyDateProps {
|
|
50
|
+
date: string;
|
|
51
|
+
showTime?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function PrettyDate(props: PrettyDateProps) {
|
|
55
|
+
return <>{getPrettyDate(props)}</>;
|
|
56
|
+
}
|