|
1 | 1 | import React, { useState, useEffect } from "react";
|
| 2 | +import Admonition from "@docusaurus/theme-classic/lib/theme/Admonition"; |
2 | 3 |
|
3 | 4 | // Function to calculate the Levenshtein distance between two strings
|
4 | 5 | function levenshteinDistance(a, b) {
|
@@ -49,137 +50,243 @@ function highlightSubstring(str, substr) {
|
49 | 50 | }
|
50 | 51 |
|
51 | 52 | const Packages = () => {
|
52 |
| - const [packages, setPackages] = useState([]); |
| 53 | + const [allPackages, setAllPackages] = useState({}); |
| 54 | + const [latestPackages, setLatestPackages] = useState([]); |
53 | 55 | const [searchTerm, setSearchTerm] = useState("");
|
54 | 56 |
|
55 | 57 | useEffect(() => {
|
56 |
| - const fetchData = async () => { |
| 58 | + const fetchAllData = async () => { |
57 | 59 | try {
|
58 |
| - const response = await fetch("https://raw.githubusercontent.com/conda-forge/feedstock-outputs/single-file/feedstock-outputs.json"); |
| 60 | + const response = await fetch( |
| 61 | + "https://raw.githubusercontent.com/conda-forge/feedstock-outputs/single-file/feedstock-outputs.json" |
| 62 | + ); |
59 | 63 | const data = await response.json();
|
60 | 64 |
|
61 | 65 | if (typeof data === "object" && data !== null) {
|
62 |
| - // Convert the object into an array of { pkg_name, repositories } objects |
63 |
| - const packagesArray = Object.entries(data).map(([name, repos]) => ({ |
64 |
| - name, |
65 |
| - repos, |
66 |
| - })); |
67 |
| - |
68 |
| - setPackages(packagesArray); |
| 66 | + setAllPackages( |
| 67 | + Object.fromEntries( |
| 68 | + Object.entries(data).map(([key, value]) => [ |
| 69 | + key.toLowerCase(), |
| 70 | + value, |
| 71 | + ]) |
| 72 | + ) |
| 73 | + ); |
69 | 74 | } else {
|
70 | 75 | console.error("Invalid data format. Expected an object.");
|
71 | 76 | }
|
72 | 77 | } catch (error) {
|
73 | 78 | console.error("Error fetching packages:", error);
|
74 | 79 | }
|
75 | 80 | };
|
76 |
| - |
77 |
| - fetchData(); |
| 81 | + const fetchLatestData = async () => { |
| 82 | + try { |
| 83 | + const response = await fetch( |
| 84 | + "https://conda.anaconda.org/conda-forge/rss.xml" |
| 85 | + ); |
| 86 | + // parse the RSS feed into an XML document |
| 87 | + const xml = await response.text(); |
| 88 | + const parser = new DOMParser(); |
| 89 | + const doc = parser.parseFromString(xml, "text/xml"); |
| 90 | + const titles = doc.querySelectorAll("title"); |
| 91 | + const dates = doc.querySelectorAll("pubDate"); |
| 92 | + // Convert the object into an array of { name, date } objects |
| 93 | + var latestPackagesArray = []; |
| 94 | + // The first 'title' element is the feed title, so we skip it |
| 95 | + titles.forEach( |
| 96 | + (title, index) => |
| 97 | + index && |
| 98 | + latestPackagesArray.push({ |
| 99 | + name: title.textContent.split(" ")[0], |
| 100 | + date: dates[index - 1].textContent, |
| 101 | + }) |
| 102 | + ); |
| 103 | + setLatestPackages(latestPackagesArray); |
| 104 | + } catch (error) { |
| 105 | + console.error("Error fetching latest packages:", error); |
| 106 | + } |
| 107 | + }; |
| 108 | + fetchLatestData(); |
| 109 | + fetchAllData(); |
78 | 110 | }, []);
|
79 | 111 |
|
80 | 112 | const searchTermLower = searchTerm.toLowerCase();
|
81 | 113 | var filteredPackages = [];
|
82 |
| - if (searchTerm.length >= 3) { |
83 |
| - // For queries with three or more characters, search the entire string for a match |
84 |
| - filteredPackages = packages.filter((pkg) => |
85 |
| - pkg.name.toLowerCase().includes(searchTermLower) |
86 |
| - ); |
87 |
| - } else if (searchTerm.length > 0) { |
88 |
| - // For queries with less than three characters, |
89 |
| - // only search if the package name starts with the query for performance reasons |
90 |
| - filteredPackages = packages.filter((pkg) => |
91 |
| - pkg.name.toLowerCase().startsWith(searchTermLower) |
92 |
| - ); |
| 114 | + var inclusionCriteria; |
| 115 | + if (searchTerm.length > 0) { |
| 116 | + if (searchTerm.length >= 3) { |
| 117 | + inclusionCriteria = (name) => name.includes(searchTermLower); |
| 118 | + } else { |
| 119 | + inclusionCriteria = (name) => name.startsWith(searchTermLower); |
| 120 | + } |
| 121 | + for (const name in allPackages) { |
| 122 | + if (inclusionCriteria(name)) { |
| 123 | + filteredPackages.push(name); |
| 124 | + } |
| 125 | + } |
93 | 126 | }
|
| 127 | + |
94 | 128 | // Sort the filtered packages in place by their Levenshtein distance
|
95 | 129 | filteredPackages.sort((a, b) => {
|
96 |
| - const aDistance = levenshteinDistance( |
97 |
| - a.name.toLowerCase(), |
98 |
| - searchTermLower |
99 |
| - ); |
100 |
| - const bDistance = levenshteinDistance( |
101 |
| - b.name.toLowerCase(), |
102 |
| - searchTermLower |
103 |
| - ); |
| 130 | + const aDistance = levenshteinDistance(a, searchTermLower); |
| 131 | + const bDistance = levenshteinDistance(b, searchTermLower); |
104 | 132 | return aDistance - bDistance;
|
105 | 133 | });
|
106 | 134 |
|
107 | 135 | const handleSearchChange = (event) => {
|
108 | 136 | setSearchTerm(event.target.value);
|
109 | 137 | };
|
110 | 138 |
|
| 139 | + var renderResultsBlock; |
| 140 | + var resultsPill; |
| 141 | + if (searchTerm.length) { |
| 142 | + // This is the results table, displayed when the user enters a search term |
| 143 | + renderResultsBlock = ( |
| 144 | + <table> |
| 145 | + <thead> |
| 146 | + <tr> |
| 147 | + <th>Package</th> |
| 148 | + <th>Feedstock(s)</th> |
| 149 | + </tr> |
| 150 | + </thead> |
| 151 | + <tbody> |
| 152 | + {(filteredPackages.length && |
| 153 | + filteredPackages.map((pkg) => ( |
| 154 | + <tr key={pkg}> |
| 155 | + <td> |
| 156 | + <a |
| 157 | + href={`https://anaconda.org/conda-forge/${pkg}`} |
| 158 | + target="_blank" |
| 159 | + title={`View ${pkg} on anaconda.org`} |
| 160 | + > |
| 161 | + {highlightSubstring(pkg, searchTermLower)} |
| 162 | + </a> |
| 163 | + </td> |
| 164 | + <td> |
| 165 | + {allPackages[pkg].map((repo) => ( |
| 166 | + <span key={`${pkg}-${repo}`}> |
| 167 | + <a |
| 168 | + href={`https://github.com/conda-forge/${repo}-feedstock`} |
| 169 | + target="_blank" |
| 170 | + title={`View ${repo}-feedstock on GitHub`} |
| 171 | + > |
| 172 | + {repo}-feedstock |
| 173 | + </a> |
| 174 | + |
| 175 | + <br /> |
| 176 | + </span> |
| 177 | + ))} |
| 178 | + </td> |
| 179 | + </tr> |
| 180 | + ))) || ( |
| 181 | + <tr> |
| 182 | + <td colSpan="2">No packages found</td> |
| 183 | + </tr> |
| 184 | + )} |
| 185 | + </tbody> |
| 186 | + </table> |
| 187 | + ); |
| 188 | + resultsPill = ( |
| 189 | + <span className="badge badge--secondary margin-left--sm"> |
| 190 | + {filteredPackages.length} package(s) found |
| 191 | + </span> |
| 192 | + ); |
| 193 | + } else { |
| 194 | + // Without a search term, display the most recently updated feedstocks |
| 195 | + renderResultsBlock = ( |
| 196 | + <div> |
| 197 | + <Admonition type="tip" coll> |
| 198 | + <p> |
| 199 | + The following packages have recently received updates in{" "} |
| 200 | + <a |
| 201 | + href="https://anaconda.org/conda-forge" |
| 202 | + target="_blank" |
| 203 | + rel="noopener noreferrer" |
| 204 | + > |
| 205 | + Anaconda.org |
| 206 | + </a> |
| 207 | + . Check{" "} |
| 208 | + <a |
| 209 | + href="https://github.com/conda-forge/feedstocks/commits" |
| 210 | + target="_blank" |
| 211 | + rel="noopener noreferrer" |
| 212 | + > |
| 213 | + conda-forge/feedstocks |
| 214 | + </a>{" "} |
| 215 | + for an overview of the latest commits in our feedstocks. |
| 216 | + </p> |
| 217 | + </Admonition> |
| 218 | + <table> |
| 219 | + <thead> |
| 220 | + <tr> |
| 221 | + <th>#</th> |
| 222 | + <th>Package</th> |
| 223 | + <th>Feedstock(s)</th> |
| 224 | + <th>Last updated</th> |
| 225 | + </tr> |
| 226 | + </thead> |
| 227 | + <tbody> |
| 228 | + {latestPackages.map((item, index) => ( |
| 229 | + <tr key={item.name}> |
| 230 | + <td>{index + 1}</td> |
| 231 | + <td> |
| 232 | + <a |
| 233 | + href={`https://anaconda.org/conda-forge/${item.name}`} |
| 234 | + target="_blank" |
| 235 | + rel="noopener noreferrer" |
| 236 | + > |
| 237 | + {item.name} |
| 238 | + </a> |
| 239 | + </td> |
| 240 | + <td> |
| 241 | + {(allPackages[item.name.toLowerCase()] || []).map((repo) => ( |
| 242 | + <span key={`${item.name}-${index}-${repo}`}> |
| 243 | + <a |
| 244 | + href={`https://github.com/conda-forge/${repo}-feedstock`} |
| 245 | + target="_blank" |
| 246 | + rel="noopener noreferrer" |
| 247 | + title={`View ${repo}-feedstock on GitHub`} |
| 248 | + > |
| 249 | + {repo}-feedstock |
| 250 | + </a> |
| 251 | + <br /> |
| 252 | + </span> |
| 253 | + ))} |
| 254 | + </td> |
| 255 | + <td>{item.date}</td> |
| 256 | + </tr> |
| 257 | + ))} |
| 258 | + </tbody> |
| 259 | + </table> |
| 260 | + </div> |
| 261 | + ); |
| 262 | + resultsPill = ( |
| 263 | + <span className="badge badge--secondary margin-left--sm"> |
| 264 | + {Object.keys(allPackages).length} packages loaded |
| 265 | + </span> |
| 266 | + ); |
| 267 | + } |
| 268 | + |
111 | 269 | return (
|
112 |
| - <div |
113 |
| - className={["container", "margin-vert--lg"].join(" ")} |
114 |
| - > |
| 270 | + <div className={["container", "margin-vert--lg"].join(" ")}> |
115 | 271 | <div className="row">
|
116 | 272 | <main className="col col--12">
|
117 | 273 | <h1>Packages in conda-forge</h1>
|
118 |
| - <form className="margin-vert--md"> |
| 274 | + <form id="filterPackages" className="margin-vert--md"> |
119 | 275 | <div className="navbar__search">
|
120 |
| - <label htmlFor="search"> |
| 276 | + <label htmlFor="filterPackagesInput"> |
121 | 277 | <input
|
| 278 | + id="filterPackagesInput" |
122 | 279 | type="text"
|
123 | 280 | placeholder="Filter items..."
|
124 | 281 | value={searchTerm}
|
125 | 282 | onChange={handleSearchChange}
|
126 | 283 | className="navbar__search-input"
|
127 | 284 | />
|
128 |
| - {(searchTerm.length && ( |
129 |
| - <span class="badge badge--info margin-left--sm"> |
130 |
| - {filteredPackages.length} package(s) found |
131 |
| - </span> |
132 |
| - )) || ( |
133 |
| - <span class="badge badge--success margin-left--sm"> |
134 |
| - {packages.length} packages loaded |
135 |
| - </span> |
136 |
| - )} |
| 285 | + {resultsPill} |
137 | 286 | </label>
|
138 | 287 | </div>
|
139 | 288 | </form>
|
140 |
| - <table> |
141 |
| - <thead> |
142 |
| - <tr> |
143 |
| - <th>Package</th> |
144 |
| - <th>Feedstock(s)</th> |
145 |
| - </tr> |
146 |
| - </thead> |
147 |
| - <tbody> |
148 |
| - {(filteredPackages.length && |
149 |
| - filteredPackages.map((pkg) => ( |
150 |
| - <tr key={pkg.name}> |
151 |
| - <td> |
152 |
| - <a |
153 |
| - href={`https://anaconda.org/conda-forge/${pkg.name}`} |
154 |
| - target="_blank" |
155 |
| - title={`View ${pkg.name} on anaconda.org`} |
156 |
| - > |
157 |
| - {highlightSubstring(pkg.name, searchTermLower)} |
158 |
| - </a> |
159 |
| - </td> |
160 |
| - <td> |
161 |
| - {pkg.repos.map((repo) => ( |
162 |
| - <span> |
163 |
| - <a |
164 |
| - href={`https://github.com/conda-forge/${repo}-feedstock`} |
165 |
| - target="_blank" |
166 |
| - title={`View ${repo}-feedstock on GitHub`} |
167 |
| - > |
168 |
| - {repo}-feedstock |
169 |
| - </a> |
170 |
| - |
171 |
| - <br /> |
172 |
| - </span> |
173 |
| - ))} |
174 |
| - </td> |
175 |
| - </tr> |
176 |
| - ))) || ( |
177 |
| - <tr> |
178 |
| - <td colSpan="2">Use the search bar to find packages</td> |
179 |
| - </tr> |
180 |
| - )} |
181 |
| - </tbody> |
182 |
| - </table> |
| 289 | + {renderResultsBlock} |
183 | 290 | </main>
|
184 | 291 | </div>
|
185 | 292 | </div>
|
|
0 commit comments