Implement a multiselect for dependencies

This commit is contained in:
Vladyslav Fedoriuk 2023-09-19 19:50:45 +02:00
parent 3596efa906
commit 2ef96977a0
17 changed files with 20750 additions and 644 deletions

View File

@ -1,4 +1,15 @@
"""Create index.json file from database."""
"""
Create repos and dependencies indexes.
This script creates can create two indexes:
- ``repos_index.json``: Contains all the repositories and their dependencies.
- ``dependencies_index.json``: Contains all the dependencies and the
repositories that depend on them.
The indexes are used by the frontend to display the data and perform searches.
"""
import asyncio
import json
from pathlib import Path
from typing import Final
@ -7,23 +18,28 @@ import aiofiles
import sqlalchemy.orm
import typer
from app.database import Repo
from app.models import RepoDetail
from app.database import Dependency, Repo
from app.models import DependencyDetail, RepoDetail
from app.uow import async_session_uow
#: The path to the index.json file.
INDEX_PATH: Final[Path] = Path(__file__).parent.parent / "index.json"
#: The path to the repos index file.
REPOS_INDEX_PATH: Final[Path] = Path(__file__).parent.parent / "repos_index.json"
#: The path to the dependencies index file.
DEPENDENCIES_INDEX_PATH: Final[Path] = (
Path(__file__).parent.parent / "dependencies_index.json"
)
app = typer.Typer()
async def create_index() -> None:
async def create_repos_index() -> None:
"""
Create index.json file from database.
Create repos_index.json file from database.
Creates an index which is going to be used by the frontend.
:return: None
"""
async with async_session_uow() as session, aiofiles.open(
INDEX_PATH, "w"
REPOS_INDEX_PATH, "w"
) as index_file:
await index_file.write(
json.dumps(
@ -44,12 +60,45 @@ async def create_index() -> None:
)
def main() -> None:
"""Create index.json file from database."""
import asyncio
async def create_dependencies_index() -> None:
"""
Create dependencies_index.json file from database.
asyncio.run(create_index())
:return: None
"""
async with async_session_uow() as session, aiofiles.open(
DEPENDENCIES_INDEX_PATH, "w"
) as index_file:
dependencies = [
DependencyDetail.model_validate(dependency).model_dump()
async for dependency in (
await session.stream_scalars(
sqlalchemy.select(Dependency).order_by(Dependency.id)
)
)
if dependency.name
]
await index_file.write(
json.dumps(
{
"dependencies": dependencies,
},
indent=4,
)
)
@app.command()
def repos() -> None:
"""Create ``repos_index.json``."""
asyncio.run(create_repos_index())
@app.command()
def dependencies() -> None:
"""Create ``dependencies_index.json``."""
asyncio.run(create_dependencies_index())
if __name__ == "__main__":
typer.run(main)
app()

17836
dependencies_index.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,15 +11,18 @@
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@orama/orama": "^1.2.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
"@tanstack/react-table": "^8.9.3",
"@tanstack/react-virtual": "3.0.0-alpha.0",
"@types/node": "20.5.1",
"@types/react": "18.2.20",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.15",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"eslint": "8.47.0",
"eslint-config-next": "13.4.18",
"lucide-react": "^0.269.0",

712
frontend/pnpm-lock.yaml generated
View File

@ -11,6 +11,9 @@ dependencies:
"@orama/orama":
specifier: ^1.2.3
version: 1.2.3
"@radix-ui/react-dialog":
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-label":
specifier: ^2.0.2
version: 2.0.2(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
@ -20,6 +23,9 @@ dependencies:
"@tanstack/react-table":
specifier: ^8.9.3
version: 8.9.3(react-dom@18.2.0)(react@18.2.0)
"@tanstack/react-virtual":
specifier: 3.0.0-alpha.0
version: 3.0.0-alpha.0(react@18.2.0)
"@types/node":
specifier: 20.5.1
version: 20.5.1
@ -38,6 +44,9 @@ dependencies:
clsx:
specifier: ^2.0.0
version: 2.0.0
cmdk:
specifier: ^0.2.0
version: 0.2.0(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
eslint:
specifier: 8.47.0
version: 8.47.0
@ -401,6 +410,36 @@ packages:
engines: { node: ">= 16.0.0" }
dev: false
/@radix-ui/primitive@1.0.0:
resolution:
{
integrity: sha512-3e7rn8FDMin4CgeL7Z/49smCA3rFYY3Ha2rUQ7HRWFadS5iCRw08ZgVT1LaNTCNqgvrUiyczLflrVrF0SRQtNA==,
}
dependencies:
"@babel/runtime": 7.22.10
dev: false
/@radix-ui/primitive@1.0.1:
resolution:
{
integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==,
}
dependencies:
"@babel/runtime": 7.22.10
dev: false
/@radix-ui/react-compose-refs@1.0.0(react@18.2.0):
resolution:
{
integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
react: 18.2.0
dev: false
/@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.20)(react@18.2.0):
resolution:
{
@ -418,6 +457,252 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-context@1.0.0(react@18.2.0):
resolution:
{
integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
react: 18.2.0
dev: false
/@radix-ui/react-context@1.0.1(@types/react@18.2.20)(react@18.2.0):
resolution:
{
integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==,
}
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
dependencies:
"@babel/runtime": 7.22.10
"@types/react": 18.2.20
react: 18.2.0
dev: false
/@radix-ui/react-dialog@1.0.0(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution:
{
integrity: sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/primitive": 1.0.0
"@radix-ui/react-compose-refs": 1.0.0(react@18.2.0)
"@radix-ui/react-context": 1.0.0(react@18.2.0)
"@radix-ui/react-dismissable-layer": 1.0.0(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-focus-guards": 1.0.0(react@18.2.0)
"@radix-ui/react-focus-scope": 1.0.0(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-id": 1.0.0(react@18.2.0)
"@radix-ui/react-portal": 1.0.0(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-presence": 1.0.0(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-primitive": 1.0.0(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-slot": 1.0.0(react@18.2.0)
"@radix-ui/react-use-controllable-state": 1.0.0(react@18.2.0)
aria-hidden: 1.2.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.4(@types/react@18.2.20)(react@18.2.0)
transitivePeerDependencies:
- "@types/react"
dev: false
/@radix-ui/react-dialog@1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution:
{
integrity: sha512-hJtRy/jPULGQZceSAP2Re6/4NpKo8im6V8P2hUqZsdFiSL8l35kYsw3qbRI6Ay5mQd2+wlLqje770eq+RJ3yZg==,
}
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-compose-refs": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@radix-ui/react-context": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@radix-ui/react-dismissable-layer": 1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-focus-guards": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@radix-ui/react-focus-scope": 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-id": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@radix-ui/react-portal": 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-presence": 1.0.1(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-primitive": 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-slot": 1.0.2(@types/react@18.2.20)(react@18.2.0)
"@radix-ui/react-use-controllable-state": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@types/react": 18.2.20
"@types/react-dom": 18.2.7
aria-hidden: 1.2.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.5(@types/react@18.2.20)(react@18.2.0)
dev: false
/@radix-ui/react-dismissable-layer@1.0.0(react-dom@18.2.0)(react@18.2.0):
resolution:
{
integrity: sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/primitive": 1.0.0
"@radix-ui/react-compose-refs": 1.0.0(react@18.2.0)
"@radix-ui/react-primitive": 1.0.0(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-use-callback-ref": 1.0.0(react@18.2.0)
"@radix-ui/react-use-escape-keydown": 1.0.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution:
{
integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==,
}
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-compose-refs": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@radix-ui/react-primitive": 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-use-callback-ref": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@radix-ui/react-use-escape-keydown": 1.0.3(@types/react@18.2.20)(react@18.2.0)
"@types/react": 18.2.20
"@types/react-dom": 18.2.7
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-focus-guards@1.0.0(react@18.2.0):
resolution:
{
integrity: sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
react: 18.2.0
dev: false
/@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.20)(react@18.2.0):
resolution:
{
integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==,
}
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
dependencies:
"@babel/runtime": 7.22.10
"@types/react": 18.2.20
react: 18.2.0
dev: false
/@radix-ui/react-focus-scope@1.0.0(react-dom@18.2.0)(react@18.2.0):
resolution:
{
integrity: sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-compose-refs": 1.0.0(react@18.2.0)
"@radix-ui/react-primitive": 1.0.0(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-use-callback-ref": 1.0.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-focus-scope@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution:
{
integrity: sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==,
}
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-compose-refs": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@radix-ui/react-primitive": 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
"@radix-ui/react-use-callback-ref": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@types/react": 18.2.20
"@types/react-dom": 18.2.7
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-id@1.0.0(react@18.2.0):
resolution:
{
integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-use-layout-effect": 1.0.0(react@18.2.0)
react: 18.2.0
dev: false
/@radix-ui/react-id@1.0.1(@types/react@18.2.20)(react@18.2.0):
resolution:
{
integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==,
}
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-use-layout-effect": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@types/react": 18.2.20
react: 18.2.0
dev: false
/@radix-ui/react-label@2.0.2(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution:
{
@ -442,6 +727,101 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-portal@1.0.0(react-dom@18.2.0)(react@18.2.0):
resolution:
{
integrity: sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-primitive": 1.0.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-portal@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution:
{
integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==,
}
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-primitive": 1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
"@types/react": 18.2.20
"@types/react-dom": 18.2.7
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-presence@1.0.0(react-dom@18.2.0)(react@18.2.0):
resolution:
{
integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-compose-refs": 1.0.0(react@18.2.0)
"@radix-ui/react-use-layout-effect": 1.0.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-presence@1.0.1(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution:
{
integrity: sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==,
}
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-compose-refs": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@radix-ui/react-use-layout-effect": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@types/react": 18.2.20
"@types/react-dom": 18.2.7
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-primitive@1.0.0(react-dom@18.2.0)(react@18.2.0):
resolution:
{
integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-slot": 1.0.0(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-primitive@1.0.3(@types/react-dom@18.2.7)(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution:
{
@ -466,6 +846,19 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-slot@1.0.0(react@18.2.0):
resolution:
{
integrity: sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-compose-refs": 1.0.0(react@18.2.0)
react: 18.2.0
dev: false
/@radix-ui/react-slot@1.0.2(@types/react@18.2.20)(react@18.2.0):
resolution:
{
@ -484,6 +877,133 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-use-callback-ref@1.0.0(react@18.2.0):
resolution:
{
integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
react: 18.2.0
dev: false
/@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.2.20)(react@18.2.0):
resolution:
{
integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==,
}
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
dependencies:
"@babel/runtime": 7.22.10
"@types/react": 18.2.20
react: 18.2.0
dev: false
/@radix-ui/react-use-controllable-state@1.0.0(react@18.2.0):
resolution:
{
integrity: sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-use-callback-ref": 1.0.0(react@18.2.0)
react: 18.2.0
dev: false
/@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.2.20)(react@18.2.0):
resolution:
{
integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==,
}
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-use-callback-ref": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@types/react": 18.2.20
react: 18.2.0
dev: false
/@radix-ui/react-use-escape-keydown@1.0.0(react@18.2.0):
resolution:
{
integrity: sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-use-callback-ref": 1.0.0(react@18.2.0)
react: 18.2.0
dev: false
/@radix-ui/react-use-escape-keydown@1.0.3(@types/react@18.2.20)(react@18.2.0):
resolution:
{
integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==,
}
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
dependencies:
"@babel/runtime": 7.22.10
"@radix-ui/react-use-callback-ref": 1.0.1(@types/react@18.2.20)(react@18.2.0)
"@types/react": 18.2.20
react: 18.2.0
dev: false
/@radix-ui/react-use-layout-effect@1.0.0(react@18.2.0):
resolution:
{
integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==,
}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
dependencies:
"@babel/runtime": 7.22.10
react: 18.2.0
dev: false
/@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.2.20)(react@18.2.0):
resolution:
{
integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==,
}
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
dependencies:
"@babel/runtime": 7.22.10
"@types/react": 18.2.20
react: 18.2.0
dev: false
/@reach/observe-rect@1.2.0:
resolution:
{
integrity: sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==,
}
dev: false
/@rushstack/eslint-patch@1.3.3:
resolution:
{
@ -515,6 +1035,20 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/@tanstack/react-virtual@3.0.0-alpha.0(react@18.2.0):
resolution:
{
integrity: sha512-WpHU/dt34NwZZ8qtiE05TF+nX/b1W6qrWZarO+s8jJFpPVicrTbJKp5Bjt4eSJuk7aYw272oEfsH3ABBRgj+3A==,
}
engines: { node: ">=12" }
peerDependencies:
react: ">=16"
dependencies:
"@babel/runtime": 7.22.10
"@reach/observe-rect": 1.2.0
react: 18.2.0
dev: false
/@tanstack/table-core@8.9.3:
resolution:
{
@ -731,6 +1265,16 @@ packages:
}
dev: false
/aria-hidden@1.2.3:
resolution:
{
integrity: sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==,
}
engines: { node: ">=10" }
dependencies:
tslib: 2.6.2
dev: false
/aria-query@5.3.0:
resolution:
{
@ -1045,6 +1589,23 @@ packages:
engines: { node: ">=6" }
dev: false
/cmdk@0.2.0(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0):
resolution:
{
integrity: sha512-JQpKvEOb86SnvMZbYaFKYhvzFntWBeSZdyii0rZPhKJj9uwJBxu4DaVYDrRN7r3mPop56oPhRw+JYWTKs66TYw==,
}
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
dependencies:
"@radix-ui/react-dialog": 1.0.0(@types/react@18.2.20)(react-dom@18.2.0)(react@18.2.0)
command-score: 0.1.2
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
transitivePeerDependencies:
- "@types/react"
dev: false
/color-convert@2.0.1:
resolution:
{
@ -1062,6 +1623,13 @@ packages:
}
dev: false
/command-score@0.1.2:
resolution:
{
integrity: sha512-VtDvQpIJBvBatnONUsPzXYFVKQQAhuf3XTNOAsdBxCNO/QCtUUd8LSgjn0GVarBkCad6aJCZfXgrjYbl/KRr7w==,
}
dev: false
/commander@4.1.1:
resolution:
{
@ -1167,6 +1735,13 @@ packages:
engines: { node: ">=6" }
dev: false
/detect-node-es@1.1.0:
resolution:
{
integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==,
}
dev: false
/didyoumean@1.2.2:
resolution:
{
@ -1844,6 +2419,14 @@ packages:
has-symbols: 1.0.3
dev: false
/get-nonce@1.0.1:
resolution:
{
integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==,
}
engines: { node: ">=6" }
dev: false
/get-symbol-description@1.0.0:
resolution:
{
@ -2107,6 +2690,15 @@ packages:
side-channel: 1.0.4
dev: false
/invariant@2.2.4:
resolution:
{
integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==,
}
dependencies:
loose-envify: 1.4.0
dev: false
/is-array-buffer@3.0.2:
resolution:
{
@ -3062,6 +3654,89 @@ packages:
}
dev: false
/react-remove-scroll-bar@2.3.4(@types/react@18.2.20)(react@18.2.0):
resolution:
{
integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==,
}
engines: { node: ">=10" }
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
dependencies:
"@types/react": 18.2.20
react: 18.2.0
react-style-singleton: 2.2.1(@types/react@18.2.20)(react@18.2.0)
tslib: 2.6.2
dev: false
/react-remove-scroll@2.5.4(@types/react@18.2.20)(react@18.2.0):
resolution:
{
integrity: sha512-xGVKJJr0SJGQVirVFAUZ2k1QLyO6m+2fy0l8Qawbp5Jgrv3DeLalrfMNBFSlmz5kriGGzsVBtGVnf4pTKIhhWA==,
}
engines: { node: ">=10" }
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
dependencies:
"@types/react": 18.2.20
react: 18.2.0
react-remove-scroll-bar: 2.3.4(@types/react@18.2.20)(react@18.2.0)
react-style-singleton: 2.2.1(@types/react@18.2.20)(react@18.2.0)
tslib: 2.6.2
use-callback-ref: 1.3.0(@types/react@18.2.20)(react@18.2.0)
use-sidecar: 1.1.2(@types/react@18.2.20)(react@18.2.0)
dev: false
/react-remove-scroll@2.5.5(@types/react@18.2.20)(react@18.2.0):
resolution:
{
integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==,
}
engines: { node: ">=10" }
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
dependencies:
"@types/react": 18.2.20
react: 18.2.0
react-remove-scroll-bar: 2.3.4(@types/react@18.2.20)(react@18.2.0)
react-style-singleton: 2.2.1(@types/react@18.2.20)(react@18.2.0)
tslib: 2.6.2
use-callback-ref: 1.3.0(@types/react@18.2.20)(react@18.2.0)
use-sidecar: 1.1.2(@types/react@18.2.20)(react@18.2.0)
dev: false
/react-style-singleton@2.2.1(@types/react@18.2.20)(react@18.2.0):
resolution:
{
integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==,
}
engines: { node: ">=10" }
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
dependencies:
"@types/react": 18.2.20
get-nonce: 1.0.1
invariant: 2.2.4
react: 18.2.0
tslib: 2.6.2
dev: false
/react@18.2.0:
resolution:
{
@ -3673,6 +4348,43 @@ packages:
punycode: 2.3.0
dev: false
/use-callback-ref@1.3.0(@types/react@18.2.20)(react@18.2.0):
resolution:
{
integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==,
}
engines: { node: ">=10" }
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
dependencies:
"@types/react": 18.2.20
react: 18.2.0
tslib: 2.6.2
dev: false
/use-sidecar@1.1.2(@types/react@18.2.20)(react@18.2.0):
resolution:
{
integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==,
}
engines: { node: ">=10" }
peerDependencies:
"@types/react": ^16.9.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
dependencies:
"@types/react": 18.2.20
detect-node-es: 1.1.0
react: 18.2.0
tslib: 2.6.2
dev: false
/util-deprecate@1.0.2:
resolution:
{

View File

@ -0,0 +1,31 @@
"use client";
import {
createDependenciesOrama,
prepareDependenciesOramaIndex,
DependenciesOramaContext,
} from "@/lib/search";
import { PropsWithChildren } from "react";
import { SearchProvider } from "./search-provider";
import { DependenciesIndex } from "@/lib/schemas";
export function DependenciesSearchProvider({
children,
dependencies,
}: PropsWithChildren<{
dependencies: DependenciesIndex["dependencies"];
}>) {
const prepareOramaIndex = async () => {
const orama = await createDependenciesOrama();
await prepareDependenciesOramaIndex(orama, dependencies);
return orama;
};
return (
<SearchProvider
createIndex={prepareOramaIndex}
OramaContext={DependenciesOramaContext}
>
{children}
</SearchProvider>
);
}

View File

@ -1,14 +1,21 @@
import { loadIndexServerOnly } from "@/lib/repos-index";
import {
loadDependenciesIndexServerOnly,
loadReposIndexServerOnly,
} from "@/lib/indexes";
import { ReposTable } from "./repos-table";
import { ReposSearchProvider } from "./repos-search-provider";
import { DependenciesSearchProvider } from "./dependencies-search-provider";
export default async function Home() {
const { repos } = await loadIndexServerOnly();
const { repos } = await loadReposIndexServerOnly();
const { dependencies } = await loadDependenciesIndexServerOnly();
return (
<section className="py-10">
<ReposSearchProvider repos={repos}>
<ReposTable repos={repos} />
<DependenciesSearchProvider dependencies={dependencies}>
<ReposTable repos={repos} dependencies={dependencies} />
</DependenciesSearchProvider>
</ReposSearchProvider>
</section>
);

View File

@ -6,13 +6,13 @@ import {
} from "@/lib/search";
import { PropsWithChildren } from "react";
import { SearchProvider } from "./search-provider";
import { Index } from "@/lib/schemas";
import { RepoIndex } from "@/lib/schemas";
export function ReposSearchProvider({
children,
repos,
}: PropsWithChildren<{
repos: Index["repos"];
repos: RepoIndex["repos"];
}>) {
const prepareOramaIndex = async () => {
const orama = await createReposOrama();

View File

@ -1,5 +1,5 @@
"use client";
import { Index, Repo } from "@/lib/schemas";
import { Repo, Dependency } from "@/lib/schemas";
import { search } from "@orama/orama";
import { SearchForm } from "./search-form";
import { columns } from "./columns";
@ -7,7 +7,13 @@ import { DataTable } from "./data-table";
import { useReposOrama } from "@/lib/search";
import { useState } from "react";
export function ReposTable({ repos }: Index) {
export function ReposTable({
repos,
dependencies,
}: {
repos: Repo[];
dependencies: Dependency[];
}) {
const reposOrama = useReposOrama();
const [searchedRepos, setSearchedRepos] = useState<Repo[]>(repos);
@ -17,7 +23,7 @@ export function ReposTable({ repos }: Index) {
search: string;
}) => {
if (!reposOrama.isIndexed || !reposOrama.orama) {
throw new Error("Orama is not initialized");
throw new Error("Repos Orama is not initialized");
}
const results = await search<Repo>(reposOrama.orama, {
term: description,
@ -29,8 +35,8 @@ export function ReposTable({ repos }: Index) {
return (
<>
<div className="mb-4">
<SearchForm onSubmit={onSearchSubmit} />
<div className="container mb-4 max-w-xl">
<SearchForm onSubmit={onSearchSubmit} dependencies={dependencies} />
</div>
<DataTable columns={columns} data={searchedRepos} />
</>

View File

@ -14,25 +14,31 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { MultiSelect } from "@/components/ui/multiselect";
import { Dependency } from "@/lib/schemas";
const FormSchema = z.object({
search: z
.string()
.max(1024, { message: "Search must be less than 1024 characters" }),
.min(0)
.max(256, { message: "Search must be less than 256 characters" })
.default(""),
dependencies: z.array(z.string()).default(() => []),
});
export interface SearchFormProps {
onSubmit: (data: z.infer<typeof FormSchema>) => void;
dependencies: Dependency[];
}
export function SearchForm({ onSubmit }: SearchFormProps) {
export function SearchForm({ onSubmit, dependencies }: SearchFormProps) {
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="search"
@ -40,7 +46,7 @@ export function SearchForm({ onSubmit }: SearchFormProps) {
<FormItem>
<FormLabel>Search for a repository</FormLabel>
<FormControl>
<Input placeholder="fastapi" {...field} />
<Input placeholder="Search..." {...field} />
</FormControl>
<FormDescription>
The search is performed on the repository description.
@ -49,6 +55,22 @@ export function SearchForm({ onSubmit }: SearchFormProps) {
</FormItem>
)}
/>
<FormField
control={form.control}
name="dependencies"
render={({ field }) => (
<FormItem>
<FormLabel>Dependencies</FormLabel>
<FormControl>
<MultiSelect data={dependencies} {...field} />
</FormControl>
<FormDescription>
Filter by dependencies used in the repository.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Search</Button>
</form>
</Form>

View File

@ -0,0 +1,155 @@
"use client";
import * as React from "react";
import { DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className,
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
};

View File

@ -0,0 +1,123 @@
"use client";
import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = ({
className,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props} />
);
DialogPortal.displayName = DialogPrimitive.Portal.displayName;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className,
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
className,
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className,
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className,
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className,
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};

View File

@ -0,0 +1,169 @@
"use client";
import * as React from "react";
import { X } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Command, CommandGroup, CommandItem } from "@/components/ui/command";
import { Command as CommandPrimitive } from "cmdk";
import { useVirtual } from "@tanstack/react-virtual";
import { useDependenciesOrama } from "@/lib/search";
import { search } from "@orama/orama";
import { Dependency } from "@/lib/schemas";
export function MultiSelect<DataType extends { id: string; name: string }>({
data,
}: {
data: DataType[];
}) {
const inputRef = React.useRef<HTMLInputElement>(null);
const scrollableContainerRef = React.useRef(null);
const [open, setOpen] = React.useState(false);
const [selected, setSelected] = React.useState<DataType[]>([]);
const [selectables, setSelectables] = React.useState<DataType[]>(data);
const [inputValue, setInputValue] = React.useState("");
const dependenciesOrama = useDependenciesOrama();
const onChangeInputValue = async (dependencyName: string) => {
if (!dependenciesOrama.isIndexed || !dependenciesOrama.orama) {
throw new Error("Dependencies Orama is not initialized");
}
const results = await search<Dependency>(dependenciesOrama.orama, {
term: dependencyName,
properties: ["name"],
limit: data.length,
});
setSelectables(
results.hits
.map((hit) => hit.document as DataType)
.filter((dataPoint) => !selected.includes(dataPoint)),
);
setInputValue(dependencyName);
};
const rowVirtualizer = useVirtual({
size: selectables.length,
parentRef: scrollableContainerRef,
overscan: 10,
});
const handleUnselect = React.useCallback((dataPoint: DataType) => {
setSelected((prev) => prev.filter((el) => el.id !== dataPoint.id));
setSelectables((prev) => [dataPoint, ...prev]);
}, []);
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent<HTMLDivElement>) => {
const input = inputRef.current;
if (input) {
if (e.key === "Delete" || e.key === "Backspace") {
if (input.value === "") {
setSelected((prev) => {
const newSelected = [...prev];
newSelected.pop();
return newSelected;
});
}
}
// This is not a default behaviour of the <input /> field
if (e.key === "Escape") {
input.blur();
}
}
},
[],
);
return (
<Command
onKeyDown={handleKeyDown}
className="overflow-visible bg-transparent"
>
<div className="group border border-input px-3 py-2 text-sm ring-offset-background rounded-md focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
<div className="flex gap-1 flex-wrap">
{selected.map((dataPoint) => {
return (
<Badge key={dataPoint.id} variant="secondary">
{dataPoint.name}
<button
className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
onKeyDown={(e) => {
if (e.key === "Enter") {
handleUnselect(dataPoint);
}
}}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onClick={() => handleUnselect(dataPoint)}
>
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
</button>
</Badge>
);
})}
{/* Avoid having the "Search" Icon */}
<CommandPrimitive.Input
ref={inputRef}
value={inputValue}
onValueChange={onChangeInputValue}
onBlur={() => setOpen(false)}
onFocus={() => setOpen(true)}
placeholder="Select..."
className="ml-2 bg-transparent outline-none placeholder:text-muted-foreground flex-1"
/>
</div>
</div>
<div className="relative mt-2">
{open && selectables.length > 0 ? (
<div className="absolute w-full z-10 top-0 rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-in">
<CommandGroup
ref={scrollableContainerRef}
className="h-96 overflow-auto"
>
<div
style={{
height: `${rowVirtualizer.totalSize}px`,
width: "100%",
position: "relative",
}}
>
{rowVirtualizer.virtualItems.map((virtualRow) => (
<CommandItem
key={virtualRow.index}
onMouseDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onSelect={(value) => {
setInputValue("");
setSelected((prev) => [
...prev,
selectables[virtualRow.index],
]);
setSelectables((prev) =>
prev.filter((_, index) => index !== virtualRow.index),
);
}}
className="cursor-pointer"
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`,
}}
>
{selectables[virtualRow.index].name}
</CommandItem>
))}
</div>
</CommandGroup>
</div>
) : null}
</div>
</Command>
);
}

View File

@ -0,0 +1,69 @@
import { cache } from "react";
import "server-only";
import * as fs from "fs";
import * as path from "path";
import { dependenciesIndexSchema, reposIndexSchema } from "./schemas";
import { ZodError } from "zod";
// TODO: docstrings
export const REPOS_INDEX_FILE_PATH = path.normalize(
path.join(__dirname, "..", "..", "..", "..", "repos_index.json"),
);
export const DEPENDENCIES_INDEX_FILE_PATH = path.normalize(
path.join(__dirname, "..", "..", "..", "..", "dependencies_index.json"),
);
export const preload = () => {
void loadReposIndexServerOnly();
void loadDependenciesIndexServerOnly();
};
// TODO: tests
export const loadReposIndexServerOnly = cache(async () => {
try {
const indexData = JSON.parse(
await fs.promises.readFile(REPOS_INDEX_FILE_PATH, "utf-8"),
(key, value) => {
if (key === "id" || key === "source_graph_repo_id") {
return String(value);
}
return value;
},
);
return await reposIndexSchema.parseAsync(indexData);
} catch (err) {
if (err instanceof ZodError) {
throw new Error(
`Failed to parse the repos index: ${JSON.stringify(err.format())}`,
);
}
throw new Error(`Failed to load the repos index: ${err}`);
}
});
export const loadDependenciesIndexServerOnly = cache(async () => {
try {
const indexData = JSON.parse(
await fs.promises.readFile(DEPENDENCIES_INDEX_FILE_PATH, "utf-8"),
(key, value) => {
if (key === "id") {
return String(value);
}
return value;
},
);
return await dependenciesIndexSchema.parseAsync(indexData);
} catch (err) {
if (err instanceof ZodError) {
throw new Error(
`Failed to parse the dependencies index: ${JSON.stringify(
err.format(),
)}`,
);
}
throw new Error(`Failed to load the dependencies index: ${err}`);
}
});

View File

@ -1,40 +0,0 @@
import { cache } from "react";
import "server-only";
import * as fs from "fs";
import * as path from "path";
import { indexSchema } from "./schemas";
import { ZodError } from "zod";
// TODO: docstrings
export const INDEX_FILE_PATH = path.normalize(
path.join(__dirname, "..", "..", "..", "..", "index.json"),
);
export const preload = () => {
void loadIndexServerOnly();
};
// TODO: tests
export const loadIndexServerOnly = cache(async () => {
try {
const indexData = JSON.parse(
await fs.promises.readFile(INDEX_FILE_PATH, "utf-8"),
(key, value) => {
if (key === "id" || key === "source_graph_repo_id") {
return String(value);
}
return value;
},
);
return await indexSchema.parseAsync(indexData);
} catch (err) {
if (err instanceof ZodError) {
throw new Error(
`Failed to parse the repos index: ${JSON.stringify(err.format())}`,
);
}
throw new Error(`Failed to load the repos index: ${err}`);
}
});

View File

@ -15,10 +15,15 @@ export const repoSchema = z.object({
last_checked_revision: z.nullable(z.string()),
});
export const indexSchema = z.object({
export const reposIndexSchema = z.object({
repos: z.array(repoSchema),
});
export const dependenciesIndexSchema = z.object({
dependencies: z.array(dependencySchema),
});
export type Dependency = z.infer<typeof dependencySchema>;
export type Repo = z.infer<typeof repoSchema>;
export type Index = z.infer<typeof indexSchema>;
export type RepoIndex = z.infer<typeof reposIndexSchema>;
export type DependenciesIndex = z.infer<typeof dependenciesIndexSchema>;

View File

@ -4,7 +4,7 @@ import {
create,
insertMultiple,
} from "@orama/orama";
import { Index } from "./schemas";
import { DependenciesIndex, RepoIndex } from "./schemas";
import { Context, createContext, useContext } from "react";
export interface IOramaContext<
@ -34,15 +34,15 @@ export async function createReposOrama(): Promise<ReposOrama> {
schema: {
description: "string",
},
id: "index",
id: "repos-index",
});
return orama;
}
export async function prepareReposOramaIndex(
orama: ReposOrama,
data: Index["repos"],
): Promise<Orama<{ Index: { description: string } }>> {
data: RepoIndex["repos"],
): Promise<ReposOrama> {
await insertMultiple(orama, data, 100);
return orama;
}
@ -53,3 +53,36 @@ ReposOramaContext.displayName = "ReposOramaContext";
export const useReposOrama = () => {
return useContext(ReposOramaContext);
};
export interface DependenciesOramaParameters
extends Partial<OramaProvidedTypes> {
Index: { name: string };
}
export type DependenciesOrama = Orama<DependenciesOramaParameters>;
export async function createDependenciesOrama(): Promise<DependenciesOrama> {
const orama = await create({
schema: {
name: "string",
},
id: "dependencies-index",
});
return orama;
}
export async function prepareDependenciesOramaIndex(
orama: DependenciesOrama,
data: DependenciesIndex["dependencies"],
): Promise<DependenciesOrama> {
await insertMultiple(orama, data, 100);
return orama;
}
export const DependenciesOramaContext =
createOramaContext<DependenciesOramaParameters>();
DependenciesOramaContext.displayName = "DependenciesOramaContext";
export const useDependenciesOrama = () => {
return useContext(DependenciesOramaContext);
};

File diff suppressed because it is too large Load Diff