msc-viewer

A possibly better way to display the Matrix Spec Changes
git clone git://archive.git.mtrnord.blog/MTRNord/msc-viewer.git
Log | Files | Refs | README | LICENSE

results.astro (22365B)


      1 ---
      2 import StartLayout from "../layouts/StartLayout.astro";
      3 import { MeiliSearch, type SearchResponse } from "meilisearch";
      4 import Pagination from "../components/Pagination.astro";
      5 import Preview from "../components/Preview.astro";
      6 import Checkbox from "../components/Checkbox.astro";
      7 
      8 interface FilterJson {
      9     client_server?: boolean;
     10     server_server?: boolean;
     11     application_services?: boolean;
     12     e2e?: boolean;
     13     kind_core?: boolean;
     14     kind_feature?: boolean;
     15     kind_maintenance?: boolean;
     16     needs_implementation?: boolean;
     17     proposal?: boolean;
     18     abandoned?: boolean;
     19     blocked?: boolean;
     20     state?: {
     21         merged?: boolean;
     22         closed?: boolean;
     23         other?: boolean;
     24     };
     25     start_opened_date?: number;
     26     end_opened_date?: number;
     27     start_merged_closed_date?: number;
     28     end_merged_closed_date?: number;
     29     [key: string]: number | boolean | {
     30         merged?: boolean;
     31         closed?: boolean;
     32         other?: boolean;
     33     } | undefined;
     34 }
     35 
     36 const parsed_url = new URL(Astro.request.url);
     37 const search_params = parsed_url.searchParams;
     38 const query = search_params.get("q");
     39 const filter = search_params.get("filter");
     40 const current_page = search_params.get("page");
     41 const filter_parsed: FilterJson = JSON.parse(filter || "{}");
     42 let results: SearchResponse | undefined = undefined;
     43 if (Astro.request.method === "POST") {
     44     try {
     45         const data = await Astro.request.formData();
     46 
     47         const entries = Object.fromEntries(data.entries());
     48         const filter_json: FilterJson = {};
     49         for (const key in entries) {
     50             if (entries.hasOwnProperty(key)) {
     51                 if (entries[key] === "on") {
     52                      if (key === "merged" || key === "closed" || key === "other") {
     53                         filter_json["state"] = {
     54                             ...filter_json["state"],
     55                             [key]: true,
     56                         };
     57                     } else{
     58                         filter_json[key] = true;
     59                     }
     60                 } else if (
     61                     key === "start-opened-date" ||
     62                     key === "end-opened-date" ||
     63                     key === "start-merged-closed-date" ||
     64                     key === "end-merged-closed-date"
     65                 ) {
     66                     const fixed_key = key.replaceAll("-", "_");
     67                     if (entries[key] !== "") {
     68                         const milis = Date.parse(entries[key] as string);
     69                         filter_json[fixed_key] = milis / 1000;
     70                     } else {
     71                         filter_json[fixed_key] = undefined;
     72                     }
     73                 }
     74             }
     75         }
     76 
     77         const searchQuery = data.get("search");
     78         const json = JSON.stringify(filter_json);
     79         const search_params = new URLSearchParams({
     80             q: searchQuery as string,
     81             filter: json,
     82         });
     83         return Astro.redirect(`/results?${search_params.toString()}`);
     84     } catch (error) {
     85         if (error instanceof Error) {
     86             console.error(error.message);
     87         }
     88     }
     89 } else {
     90     const has_query = search_params.has("q");
     91     if (has_query) {
     92         const INDEX = import.meta.env.INDEX;
     93         const client = new MeiliSearch({
     94             host: import.meta.env.MEILI_HOST ?? "http://127.0.0.1:7700",
     95             apiKey: import.meta.env.API_KEY,
     96             requestConfig: {
     97                 headers: {
     98                     Authorization: `Bearer ${import.meta.env.API_KEY}` ?? "",
     99                 },
    100             },
    101         });
    102         results = await client.index(INDEX).search(query, {
    103             filter: filter_to_string(filter_parsed),
    104             page: parseInt(current_page ?? "1"),
    105         });
    106         console.log(results);
    107     }
    108 }
    109 
    110 function format_date(time: number | undefined): string | undefined {
    111     if (time === undefined || time === null) {
    112         return undefined;
    113     }
    114     const date = new Date(time * 1000);
    115     return date.toISOString().split("T")[0];
    116 }
    117 
    118 /** This function converts the filter json to a meilisearch filter string
    119  * We must make sure to not have a stray AND at the end.
    120  * If we compare labels we need to have an OR relation between them.
    121  * If we check state we need to check if it is merged, closed or other by comapring the state field to the string in uppercase.
    122  * Other means that it is not merged or closed.
    123  * It also can never be AND for a state as it can only be either merged, closed or other.
    124  *
    125  *
    126  * @param filter The filter json
    127  * @returns The filter string
    128  * @example
    129  * Example:
    130  * 
    131  * ```json
    132  * {
    133  *   client_server: true,
    134  *   server_server: true,
    135  *   state: {
    136  *     merged: true,
    137  *     closed: true,
    138  *   }
    139  * }
    140  * ```
    141  * 
    142  * should be converted to
    143  * 
    144  * ```text
    145  * (labels.name = "client_server" OR labels.name = "server_server") AND (state = "MERGED" OR state = "CLOSED")
    146  * ```
    147 */
    148 
    149 function filter_to_string(filter: FilterJson): string {
    150     let filter_string = "";
    151 
    152     // Copy the filter to not mutate the original
    153    const filter_copy = { ...filter };
    154     
    155     // Split the state filter from the rest
    156     const state_filter = filter_copy.state;
    157     delete filter_copy.state;
    158 
    159     // Convert the state filter to a string
    160     if (state_filter !== undefined) {
    161         if (state_filter.merged) {
    162             filter_string += `(state = "MERGED"`;
    163         }
    164         if (state_filter.closed) {
    165             if (filter_string === "") {
    166                 filter_string += `(state = "CLOSED"`;
    167             } else {
    168                 filter_string += ` OR state = "CLOSED"`;
    169             }
    170         }
    171         if (state_filter.other) {
    172             if (filter_string === "") {
    173                 filter_string += `((state != "MERGED" AND state != "CLOSED")`;
    174             } else {
    175                 filter_string += ` OR (state != "MERGED" AND state != "CLOSED")`;
    176             }
    177         }
    178         // Close the state filter
    179         if (filter_string !== "") {
    180             filter_string += ")";
    181         }
    182     }
    183 
    184     // Split the date filters from the rest
    185     const start_opened_date = filter_copy.start_opened_date;
    186     const end_opened_date = filter_copy.end_opened_date;
    187     const start_merged_closed_date = filter_copy.start_merged_closed_date;
    188     const end_merged_closed_date = filter_copy.end_merged_closed_date;
    189     delete filter_copy.start_opened_date;
    190     delete filter_copy.end_opened_date;
    191     delete filter_copy.start_merged_closed_date;
    192     delete filter_copy.end_merged_closed_date;
    193 
    194     // Convert the date filters to a string
    195     let date_filter = "(";
    196     if (start_opened_date !== undefined) {
    197         if (date_filter === "(") {
    198             date_filter += `created_at >= ${start_opened_date}`;
    199         } else {
    200             date_filter += ` AND created_at >= ${start_opened_date}`;
    201         }
    202     }
    203 
    204     if (end_opened_date !== undefined) {
    205         if (date_filter === "(") {
    206             date_filter += `created_at <= ${end_opened_date}`;
    207         } else {
    208             date_filter += ` AND created_at <= ${end_opened_date}`;
    209         }
    210     }
    211 
    212     if (start_merged_closed_date !== undefined) {
    213         if (date_filter === "(") {
    214             date_filter += `(merged_at >= ${start_merged_closed_date} AND merged_at != null) OR (closed_at >= ${start_merged_closed_date} AND closed_at != null)`;
    215         } else {
    216             date_filter += ` AND (merged_at >= ${start_merged_closed_date} AND merged_at != null) OR (closed_at >= ${start_merged_closed_date} AND closed_at != null)`;
    217         }
    218     }
    219 
    220     if (end_merged_closed_date !== undefined) {
    221         if (date_filter === "(") {
    222             date_filter += `(merged_at <= ${end_merged_closed_date} AND merged_at != null) OR (closed_at <= ${end_merged_closed_date} AND closed_at != null)`;
    223         } else {
    224             date_filter += ` AND (merged_at <= ${end_merged_closed_date} AND merged_at != null) OR (closed_at <= ${end_merged_closed_date} AND closed_at != null)`;
    225         }
    226     }
    227 
    228     // Close the date filter
    229     if (date_filter !== "") {
    230         date_filter += ")";
    231     }
    232 
    233     if (filter_string !== "" && date_filter !== "()") {
    234         filter_string += ` AND ${date_filter}`;
    235     } else if (filter_string === "" && date_filter !== "()") {
    236         filter_string += date_filter;
    237     }
    238 
    239     // Convert the rest of the filter to a string
    240     let label_filter = "(";
    241     for (const key in filter_copy) {
    242         if (filter.hasOwnProperty(key)) {
    243             const value = filter[key];
    244             if (value === undefined) {
    245                 continue;
    246             }
    247             if (typeof value === "boolean") {
    248                 if (value) {
    249                     // Add filter but make sure we don't have a stray OR at the end
    250                     if (label_filter === "(") {
    251                         label_filter += `labels.name = "${key}"`;
    252                     } else {
    253                         label_filter += ` OR labels.name = "${key}"`;
    254                     }
    255                 }
    256             }
    257         }
    258     }
    259     // Close the label filter
    260     if (label_filter !== "") {
    261         label_filter += ")";
    262     }
    263     if (filter_string !== "" && label_filter !== "()") {
    264         filter_string += ` AND ${label_filter}`;
    265     } else if (filter_string === "" && label_filter !== "()") {
    266         filter_string += label_filter;
    267     }
    268 
    269     console.log(filter_string)
    270     return filter_string;
    271 }
    272 ---
    273 
    274 <StartLayout>
    275     <form method="POST">
    276         <main>
    277             <header>
    278                 <a href="/"><h1>Matrix Spec Changes</h1></a>
    279                 <div id="search">
    280                     <input type="search" name="search" aria-label="Search" value={query} required />
    281                     <button type="submit" >
    282                         <svg id="search-icon"
    283                             width="24"
    284                             height="24"
    285                             viewBox="0 0 48 48"
    286                             fill="none"
    287                             aria-label="Search"
    288                             xmlns="http://www.w3.org/2000/svg"
    289                         >
    290                             <g clip-path="url(#clip0_2_39)">
    291                                 <path
    292                                     d="M31 28H29.42L28.86 27.46C30.82 25.18 32 22.22 32 19C32 11.82 26.18 6 19 6C11.82 6 6 11.82 6 19C6 26.18 11.82 32 19 32C22.22 32 25.18 30.82 27.46 28.86L28 29.42V31L38 40.98L40.98 38L31 28ZM19 28C14.02 28 10 23.98 10 19C10 14.02 14.02 10 19 10C23.98 10 28 14.02 28 19C28 23.98 23.98 28 19 28Z"
    293                                     fill="#334155"></path>
    294                             </g>
    295                             <defs>
    296                                 <clipPath id="clip0_2_39">
    297                                     <rect width="48" height="48" fill="white"
    298                                     ></rect>
    299                                 </clipPath>
    300                             </defs>
    301                         </svg>
    302                     </button>
    303                 </div>
    304             </header>
    305             <div id="content">
    306                 <div id="results">
    307                     {results?.hits.map((hit) => (
    308                         <Preview
    309                             title={hit.title}
    310                             author={hit.author}
    311                             content={hit.body}
    312                             id={hit.uid}
    313                         />
    314                     ))}
    315                 </div>
    316                 <aside id="filters">
    317                     <div class="filter-section">
    318                         <h2>Labels</h2>
    319                         <div class="filter-options">
    320                             <Checkbox name="client_server" label="Client-Server" checked={filter_parsed.client_server} />
    321                             <Checkbox name="server_server" label="Server-Server" checked={filter_parsed.server_server} />
    322                             <Checkbox name="application_services" label="Application Services" checked={filter_parsed.application_services} />
    323                             <Checkbox name="e2e" label="E2EE" checked={filter_parsed.e2e} />
    324                             <Checkbox name="kind_core" label="Kind: Core" checked={filter_parsed.kind_core} />
    325                             <Checkbox name="kind_feature" label="Kind: Feature" checked={filter_parsed.kind_feature} />
    326                             <Checkbox name="kind_maintenance" label="Kind: Maintenance" checked={filter_parsed.kind_maintenance} />
    327                             <Checkbox name="needs_implementation" label="Needs Implementation" checked={filter_parsed.needs_implementation} />
    328                             <Checkbox name="proposal" label="Proposal" checked={filter_parsed.proposal} />
    329                             <Checkbox name="abandoned" label="Abandoned" checked={filter_parsed.abandoned} />
    330                             <Checkbox name="blocked" label="Blocked" checked={filter_parsed.blocked} />
    331                         </div>
    332                     </div>
    333                     <div class="filter-section">
    334                         <h2>State</h2>
    335                         <div class="filter-options">
    336                             <Checkbox name="merged" label="Merged" checked={filter_parsed.state?.merged} />
    337                             <Checkbox name="closed" label="Closed" checked={filter_parsed.state?.closed} />
    338                             <Checkbox name="other" label="Other" checked={filter_parsed.state?.other} />
    339                         </div>
    340                     </div>
    341 
    342                     <div class="filter-section">
    343                         <h2>Opened Between</h2>
    344                         <div class="filter-options">
    345                             <div class="filter-wrapper date">
    346                                 <label for="start-opened-date">Start</label>
    347                                 <input
    348                                     type="date"
    349                                     name="start-opened-date"
    350                                     id="start-opened-date"
    351                                     value={format_date(
    352                                         filter_parsed.start_opened_date,
    353                                     )}
    354                                 />
    355                             </div>
    356                             <div class="filter-wrapper date">
    357                                 <label for="end-opened-date">End</label>
    358                                 <input
    359                                     type="date"
    360                                     name="end-opened-date"
    361                                     id="end-opened-date"
    362                                     value={format_date(
    363                                         filter_parsed.end_opened_date,
    364                                     )}
    365                                 />
    366                             </div>
    367                         </div>
    368                     </div>
    369 
    370                     <div class="filter-section">
    371                         <h2>Merged/closed Between</h2>
    372                         <div class="filter-options">
    373                             <div class="filter-wrapper date">
    374                                 <label for="start-merged-closed-date"
    375                                     >Start</label
    376                                 >
    377                                 <input
    378                                     type="date"
    379                                     name="start-merged-closed-date"
    380                                     id="start-merged-closed-date"
    381                                     value={filter_parsed.start_merged_closed_date}
    382                                 />
    383                             </div>
    384                             <div class="filter-wrapper date">
    385                                 <label for="end-merged-closed-date">End</label>
    386                                 <input
    387                                     type="date"
    388                                     name="end-merged-closed-date"
    389                                     id="end-merged-closed-date"
    390                                     value={filter_parsed.end_merged_closed_date}
    391                                 />
    392                             </div>
    393                         </div>
    394                     </div>
    395 
    396                     <button type="submit">Apply Filters</button>
    397                 </aside>
    398             </div>
    399             <footer>
    400                 {results ? <span id="infos">Aprox. results: {results.totalHits} ({results.processingTimeMs} ms)</span>:<></>}
    401                 <Pagination
    402                     currentPage={parseInt(current_page ?? "1")}
    403                     pages={results?.totalPages ?? 1}
    404                 />
    405             </footer>
    406         </main>
    407     </form>
    408 </StartLayout>
    409 
    410 <style lang="scss">
    411     main {
    412         display: flex;
    413         width: 100%;
    414         height: 100%;
    415         padding: var(--spacing-7, 28px) var(--spacing-6, 24px);
    416         flex-direction: column;
    417         align-items: center;
    418         gap: var(--spacing-12, 48px);
    419 
    420         header {
    421             display: flex;
    422             align-items: center;
    423             gap: var(--spacing-12, 48px);
    424             align-self: stretch;
    425 
    426             h1 {
    427                 color: var(--colors-base-white, #fff);
    428                 text-align: center;
    429                 font-size: 24px;
    430                 font-style: normal;
    431                 font-weight: 700;
    432             }
    433         }
    434 
    435         #search {
    436             width: 730px;
    437             height: 50px;
    438             display: flex;
    439             border-radius: var(--border-radius-lg, 8px);
    440             border: 2px solid var(--colors-slate-400, #94a3b8);
    441             background: var(--colors-neutral-100, #f5f5f5);
    442 
    443             /* shadow/lg */
    444             box-shadow:
    445                 0px 4px 6px -2px rgba(0, 0, 0, 0.05),
    446                 0px 10px 15px -3px rgba(0, 0, 0, 0.1);
    447 
    448             align-items: center;
    449 
    450             input[type="search"] {
    451                 width: 100%;
    452                 height: 100%;
    453                 border-top-left-radius: var(--border-radius-lg, 8px);
    454                 border-bottom-left-radius: var(--border-radius-lg, 8px);
    455                 background: var(--colors-neutral-100, #f5f5f5);
    456                 padding-left: var(--spacing-2, 8px);
    457 
    458                 color: #000;
    459                 font-size: 24px;
    460                 font-style: normal;
    461                 font-weight: 400;
    462             }
    463 
    464             #search-icon {
    465                 margin-right: var(--spacing-2, 8px);
    466                 margin-left: var(--spacing-2, 8px);
    467                 min-width: 24px;
    468                 height: 24px;
    469                 vertical-align: middle;
    470             }
    471         }
    472 
    473         #content {
    474             display: flex;
    475             align-items: flex-start;
    476             gap: var(--spacing-2, 8px);
    477             align-self: stretch;
    478             min-height: 100%;
    479 
    480             #results {
    481                 display: flex;
    482                 padding: 8px 0px 8px 8px;
    483                 flex-direction: column;
    484                 align-items: flex-start;
    485                 gap: var(--spacing-4, 16px);
    486                 flex: 1 0 0;
    487                 min-height: 100%;
    488             }
    489 
    490             #filters {
    491                 display: flex;
    492                 min-width: 350px;
    493                 padding: 8px 0px 8px 8px;
    494                 flex-direction: column;
    495                 align-items: center;
    496                 gap: 8px;
    497                 align-self: stretch;
    498                 border-left-width: 1px;
    499                 border-left-color: var(--colors-slate-500, #64748b);
    500                 min-height: 100%;
    501 
    502                 button[type="submit"]:hover {
    503                     background: var(--colors-neutral-200, #e5e5e5);
    504                 }
    505                 button[type="submit"] {
    506                     border-radius: 25px 25px 25px 25px;
    507                     background: var(--colors-neutral-100, #f5f5f5);
    508                     padding: 8px;
    509                 }
    510 
    511                 .filter-section {
    512                     display: flex;
    513                     min-width: 100%;
    514                     flex-direction: column;
    515                     align-items: center;
    516                     gap: 8px;
    517                     align-self: stretch;
    518                     color: var(--colors-base-white, #fff);
    519                     font-size: 24px;
    520                     font-style: normal;
    521                     font-weight: 700;
    522 
    523                     .filter-options {
    524                         display: flex;
    525                         padding: var(--spacing-2, 8px);
    526                         flex-direction: column;
    527                         align-items: flex-start;
    528                         gap: var(--spacing-1, 4px);
    529                         align-self: stretch;
    530 
    531                         .filter-wrapper {
    532                             color: var(--colors-base-white, #fff);
    533                             font-size: 18px;
    534                             font-style: normal;
    535                             font-weight: 400;
    536 
    537                             display: flex;
    538                             align-items: center;
    539                             gap: 8px;
    540                         }
    541                         .date {
    542                             display: flex;
    543                             flex-direction: column;
    544                             justify-content: center;
    545                             align-items: flex-start;
    546                             gap: 8px;
    547                             align-self: stretch;
    548 
    549                             input {
    550                                 background-color: var(
    551                                     --colors-slate-700,
    552                                     #334155
    553                                 );
    554                                 border-radius: var(
    555                                     --border-radius-default,
    556                                     4px
    557                                 );
    558                                 border: 1px solid
    559                                     var(--colors-slate-400, #94a3b8);
    560                                 height: 40px;
    561                                 min-width: 100%;
    562                             }
    563                         }
    564                     }
    565                 }
    566             }
    567         }
    568         footer {
    569             display: flex;
    570             flex-direction: column;
    571             align-items: center;
    572             gap: var(--spacing-2, 8px);
    573             align-self: stretch;
    574 
    575             #infos {
    576                 color: var(--colors-gray-300, #d1d5db);
    577                 text-align: center;
    578                 font-size: 16px;
    579                 font-style: normal;
    580                 font-weight: 400;
    581             }
    582         }
    583     }
    584 </style>