From 579531777faaca89723ea42e9892974d11f9287a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sun, 17 Sep 2023 18:00:16 +0200 Subject: [PATCH 01/40] Remove putStrLn --- src/web/FloraWeb/Pages/Server/Packages.hs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/web/FloraWeb/Pages/Server/Packages.hs b/src/web/FloraWeb/Pages/Server/Packages.hs index b7eacf3f..30cc7b7d 100644 --- a/src/web/FloraWeb/Pages/Server/Packages.hs +++ b/src/web/FloraWeb/Pages/Server/Packages.hs @@ -19,7 +19,6 @@ import Lucid import Lucid.Orphans () import Servant (ServerT) -import Control.Monad.IO.Class import Flora.Logging import Flora.Model.Package import Flora.Model.Package.Query qualified as Query @@ -84,7 +83,6 @@ showPackageVersion namespace packageName mversion = do templateEnv' <- fromSession session defaultTemplateEnv package <- guardThatPackageExists namespace packageName (\_ _ -> web404) releases <- Query.getReleases (package.packageId) - liftIO $ putStrLn $ "Number of releases: " <> show (length releases) let latestRelease = releases & Vector.filter (\r -> not (fromMaybe False r.deprecated)) From bc2a869846b1c1caf1320af827faa15b9f395aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sun, 17 Sep 2023 21:25:28 +0200 Subject: [PATCH 02/40] [NO-ISSUE] Fix logging domain for stream import --- src/core/Flora/Import/Package/Bulk.hs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/Flora/Import/Package/Bulk.hs b/src/core/Flora/Import/Package/Bulk.hs index 2bb89e6d..95f51ea5 100644 --- a/src/core/Flora/Import/Package/Bulk.hs +++ b/src/core/Flora/Import/Package/Bulk.hs @@ -4,6 +4,7 @@ module Flora.Import.Package.Bulk (importAllFilesInDirectory, importAllFilesInRelativeDirectory, importFromIndex) where import Codec.Archive.Tar qualified as Tar +import Codec.Archive.Tar.Entry qualified as Tar import Codec.Compression.GZip qualified as GZip import Control.Monad (join, when, (>=>)) import Data.ByteString qualified as BS @@ -13,6 +14,7 @@ import Data.List (isSuffixOf) import Data.Maybe (fromMaybe, isNothing) import Data.Text (Text) import Data.Text qualified as Text +import Data.Time (UTCTime) import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import Effectful import Effectful.Log qualified as Log @@ -26,8 +28,6 @@ import System.Directory qualified as System import System.FilePath import UnliftIO.Exception (finally) -import Codec.Archive.Tar.Entry qualified as Tar -import Data.Time (UTCTime) import Flora.Environment.Config (PoolConfig (..)) import Flora.Import.Package (enqueueImportJob, extractPackageDataFromCabal, loadContent, persistImportOutput, withWorkerDbPool) import Flora.Model.Package.Update qualified as Update @@ -139,7 +139,7 @@ importFromStream appLogger user repository directImport stream = do . runReader poolConfig . runDB pool . runTime - . Log.runLog "flora-jobs" appLogger defaultLogLevel + . Log.runLog "flora-cli" appLogger defaultLogLevel . ( \(path, timestamp, content) -> loadContent path content >>= ( extractPackageDataFromCabal user repository timestamp From 82e08896ebce9f026943c2454047689811cde05a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 23:01:15 +0200 Subject: [PATCH 03/40] Bump docker/login-action from 2 to 3 (#435) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index ac1daead..6c68ed44 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} From 6605a3a5d62528891cf697acf8d704b296ddd1d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sun, 1 Oct 2023 19:04:28 +0200 Subject: [PATCH 04/40] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5647c554..141d13ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ The compiler version used is described in the `cabal.project` file. The following Haskell command-line tools will have to be installed: * `postgresql-migration`: To perform schema migrations -* `fourmolu`: To style the code base. Minimum version is 0.12.0.0 +* `fourmolu`: To style the code base. Version is 0.12.0.0 * `hlint` & `apply-refact`: To enforce certain patterns in the code base ("lint") * `cabal-fmt` and `nixfmt`: To style the cabal and nix files * `ghcid`: To automatically reload the Haskell code base upon source changes From 27368d2d9373736b03ab630ba183d07417d731ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sun, 1 Oct 2023 19:08:37 +0200 Subject: [PATCH 05/40] [FLORA-438] Colourise in red deprecation markers on the package page (#439) --- CHANGELOG.md | 4 + assets/css/3-screens/1-package/1-package.css | 8 +- src/core/Flora/Model/Package/Query.hs | 3 +- src/core/Flora/Model/Release/Query.hs | 29 ++- src/web/FloraWeb/Pages/Server/Packages.hs | 33 ++-- src/web/FloraWeb/Pages/Templates/Packages.hs | 169 +++++++++--------- .../Pages/Templates/Pages/Packages.hs | 13 +- 7 files changed, 130 insertions(+), 129 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bef97265..eb7ebe02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 1.0.14 -- XXXX-XX-XX + +* Colourise in red deprecation markers on the package page ([#438](/~https://github.com/flora-pm/flora-server/pull/439)) + ## 1.0.13 -- 2023-09-17 * Exclude deprecated releases from latest versions and search ([#373](/~https://github.com/flora-pm/flora-server/pull/373)) * Add namespace browsing ([#375](/~https://github.com/flora-pm/flora-server/pull/375)) diff --git a/assets/css/3-screens/1-package/1-package.css b/assets/css/3-screens/1-package/1-package.css index 06b15333..4a04a57f 100644 --- a/assets/css/3-screens/1-package/1-package.css +++ b/assets/css/3-screens/1-package/1-package.css @@ -24,6 +24,10 @@ line-height: 1.75rem; } +.release-deprecated { + color: var(--deprecated-version); +} + .package-body { justify-content: center; display: grid; @@ -45,10 +49,6 @@ margin-bottom: 0.75rem; } - .release-deprecated { - color: var(--deprecated-version); - } - .package-right-column { order: 3; } diff --git a/src/core/Flora/Model/Package/Query.hs b/src/core/Flora/Model/Package/Query.hs index 77c501be..9e63e29d 100644 --- a/src/core/Flora/Model/Package/Query.hs +++ b/src/core/Flora/Model/Package/Query.hs @@ -92,7 +92,8 @@ getPackageDependents namespace packageName = dbtToEff $ query Select q (namespac getNumberOfPackageDependents :: DB :> es => Namespace -> PackageName -> Eff es Word getNumberOfPackageDependents namespace packageName = dbtToEff $ do - (result :: Maybe (Only Int)) <- queryOne Select numberOfPackageDependentsQuery (namespace, packageName) + (result :: Maybe (Only Int)) <- + queryOne Select numberOfPackageDependentsQuery (namespace, packageName) case result of Just (Only n) -> pure $ fromIntegral n Nothing -> pure 0 diff --git a/src/core/Flora/Model/Release/Query.hs b/src/core/Flora/Model/Release/Query.hs index 0f4dd4b9..101336c0 100644 --- a/src/core/Flora/Model/Release/Query.hs +++ b/src/core/Flora/Model/Release/Query.hs @@ -3,8 +3,8 @@ module Flora.Model.Release.Query ( getReleases + , getRelease , getReleaseByVersion - , getPackageReleases , getPackageReleasesWithoutReadme , getPackageReleasesWithoutChangelog , getPackageReleasesWithoutUploadTimestamp @@ -76,20 +76,6 @@ getVersionFromManyReleaseIds releaseIds = do where r0.release_id in ? |] -getPackageReleases :: DB :> es => Eff es (Vector (ReleaseId, Version, PackageName)) -getPackageReleases = - dbtToEff $ - query Select querySpec () - where - querySpec :: Query - querySpec = - [sql| - select r.release_id, r.version, p."name" - from releases as r - join packages as p - on p.package_id = r.package_id - |] - getPackageReleasesWithoutReadme :: DB :> es => Eff es (Vector (ReleaseId, Version, PackageName)) @@ -165,6 +151,19 @@ getReleaseByVersion packageId version = dbtToEff $ queryOne Select (_selectWhere @Release [[field| package_id |], [field| version |]]) (packageId, version) +getRelease + :: DB :> es + => Namespace + -> PackageName + -> Version + -> Eff es (Maybe Release) +getRelease namespace packageName version = + dbtToEff $ + queryOne + Select + (_selectWhere @Release [[field| namespace |], [field| package_name |], [field| version |]]) + (namespace, packageName, version) + getNumberOfReleases :: DB :> es => PackageId -> Eff es Word getNumberOfReleases pid = dbtToEff $ do diff --git a/src/web/FloraWeb/Pages/Server/Packages.hs b/src/web/FloraWeb/Pages/Server/Packages.hs index 30cc7b7d..cc83e5c8 100644 --- a/src/web/FloraWeb/Pages/Server/Packages.hs +++ b/src/web/FloraWeb/Pages/Server/Packages.hs @@ -70,8 +70,7 @@ showNamespaceHandler namespace pageParam = do Search.showAllPackagesInNamespace namespace count' pageNumber results showPackageHandler :: Namespace -> PackageName -> FloraPage (Html ()) -showPackageHandler namespace packageName = do - showPackageVersion namespace packageName Nothing +showPackageHandler namespace packageName = showPackageVersion namespace packageName Nothing showVersionHandler :: Namespace -> PackageName -> Version -> FloraPage (Html ()) showVersionHandler namespace packageName version = @@ -82,17 +81,17 @@ showPackageVersion namespace packageName mversion = do session <- getSession templateEnv' <- fromSession session defaultTemplateEnv package <- guardThatPackageExists namespace packageName (\_ _ -> web404) - releases <- Query.getReleases (package.packageId) + releases <- Query.getReleases package.packageId let latestRelease = releases - & Vector.filter (\r -> not (fromMaybe False r.deprecated)) + & Vector.filter (\r -> Just True /= r.deprecated) & maximumBy (compare `on` (.version)) version = fromMaybe latestRelease.version mversion release <- guardThatReleaseExists package.packageId version $ const web404 numberOfReleases <- Query.getNumberOfReleases package.packageId dependents <- Query.getPackageDependents namespace packageName releaseDependencies <- Query.getRequirements release.releaseId - categories <- Query.getPackageCategories (package.packageId) + categories <- Query.getPackageCategories package.packageId numberOfDependents <- Query.getNumberOfPackageDependents namespace packageName numberOfDependencies <- Query.getNumberOfPackageRequirements release.releaseId @@ -136,16 +135,18 @@ showPackageVersion namespace packageName mversion = do showDependentsHandler :: Namespace -> PackageName -> Maybe (Positive Word) -> FloraPage (Html ()) showDependentsHandler namespace packageName mPage = do package <- guardThatPackageExists namespace packageName (\_ _ -> web404) - releases <- Query.getAllReleases (package.packageId) + releases <- Query.getAllReleases package.packageId let latestRelease = maximumBy (compare `on` (.version)) releases showVersionDependentsHandler namespace packageName latestRelease.version mPage showVersionDependentsHandler :: Namespace -> PackageName -> Version -> Maybe (Positive Word) -> FloraPage (Html ()) -showVersionDependentsHandler namespace packageName version Nothing = showVersionDependentsHandler namespace packageName version (Just $ PositiveUnsafe 1) +showVersionDependentsHandler namespace packageName version Nothing = + showVersionDependentsHandler namespace packageName version (Just $ PositiveUnsafe 1) showVersionDependentsHandler namespace packageName version (Just pageNumber) = do session <- getSession templateEnv' <- fromSession session defaultTemplateEnv - _ <- guardThatPackageExists namespace packageName (\_ _ -> web404) + package <- guardThatPackageExists namespace packageName (\_ _ -> web404) + release <- guardThatReleaseExists package.packageId version (const web404) let templateEnv = templateEnv' { title = display namespace <> "/" <> display packageName @@ -157,7 +158,7 @@ showVersionDependentsHandler namespace packageName version (Just pageNumber) = d Package.showDependents namespace packageName - version + release totalDependents results pageNumber @@ -165,7 +166,7 @@ showVersionDependentsHandler namespace packageName version (Just pageNumber) = d showDependenciesHandler :: Namespace -> PackageName -> FloraPage (Html ()) showDependenciesHandler namespace packageName = do package <- guardThatPackageExists namespace packageName (\_ _ -> web404) - releases <- Query.getAllReleases (package.packageId) + releases <- Query.getAllReleases package.packageId let latestRelease = maximumBy (compare `on` (.version)) releases showVersionDependenciesHandler namespace packageName latestRelease.version @@ -182,7 +183,7 @@ showVersionDependenciesHandler namespace packageName version = do } (releaseDependencies, duration) <- timeAction $ - Query.getAllRequirements (release.releaseId) + Query.getAllRequirements release.releaseId Log.logInfo "Retrieving all dependencies of the latest release of a package" $ object @@ -194,14 +195,14 @@ showVersionDependenciesHandler namespace packageName version = do ] render templateEnv $ - Package.showDependencies namespace packageName version releaseDependencies + Package.showDependencies namespace packageName release releaseDependencies showChangelogHandler :: Namespace -> PackageName -> FloraPage (Html ()) showChangelogHandler namespace packageName = do package <- guardThatPackageExists namespace packageName (\_ _ -> web404) - releases <- Query.getAllReleases (package.packageId) + releases <- Query.getAllReleases package.packageId let latestRelease = maximumBy (compare `on` (.version)) releases - showVersionChangelogHandler namespace packageName (latestRelease.version) + showVersionChangelogHandler namespace packageName latestRelease.version showVersionChangelogHandler :: Namespace -> PackageName -> Version -> FloraPage (Html ()) showVersionChangelogHandler namespace packageName version = do @@ -216,7 +217,7 @@ showVersionChangelogHandler namespace packageName version = do , description = "Changelog of @" <> display namespace <> display packageName } - render templateEnv $ Package.showChangelog namespace packageName version (release.changelog) + render templateEnv $ Package.showChangelog namespace packageName version release.changelog listVersionsHandler :: Namespace -> PackageName -> FloraPage (Html ()) listVersionsHandler namespace packageName = do @@ -228,5 +229,5 @@ listVersionsHandler namespace packageName = do { title = display namespace <> "/" <> display packageName , description = "Releases of " <> display namespace <> display packageName } - releases <- Query.getAllReleases (package.packageId) + releases <- Query.getAllReleases package.packageId render templateEnv $ Package.listVersions namespace packageName releases diff --git a/src/web/FloraWeb/Pages/Templates/Packages.hs b/src/web/FloraWeb/Pages/Templates/Packages.hs index 062d2fe3..e24a4b99 100644 --- a/src/web/FloraWeb/Pages/Templates/Packages.hs +++ b/src/web/FloraWeb/Pages/Templates/Packages.hs @@ -27,7 +27,7 @@ import Text.PrettyPrint (Doc, hcat, render) import Text.PrettyPrint qualified as PP import Data.Foldable (fold) -import Data.Maybe (fromJust, fromMaybe) +import Data.Maybe (fromJust) import Distribution.Pretty (pretty) import Flora.Model.Category.Types import Flora.Model.Package @@ -55,56 +55,54 @@ instance Display Target where presentationHeaderForSubpage :: Namespace -> PackageName - -> Version + -> Release -> Target -> Word -> FloraHTML -presentationHeaderForSubpage namespace packageName version target numberOfPackages = do - div_ [class_ "divider"] $ do - div_ [class_ "page-title"] $ do - h1_ [class_ ""] $ do - span_ [class_ "headline"] $ do - displayNamespace namespace - chevronRightOutline - linkToPackageWithVersion namespace packageName version - chevronRightOutline - toHtml (display target) - p_ [class_ "synopsis"] $ - span_ [class_ "version"] $ - toHtml $ - display numberOfPackages <> " results" +presentationHeaderForSubpage namespace packageName release target numberOfPackages = div_ [class_ "divider"] $ do + div_ [class_ "page-title"] $ do + h1_ [class_ ""] $ do + span_ [class_ "headline"] $ do + displayNamespace namespace + chevronRightOutline + linkToPackageWithVersion namespace packageName release.version + chevronRightOutline + toHtml (display target) + p_ [class_ "synopsis"] $ do + span_ [class_ "version"] $ + toHtml $ + display numberOfPackages <> " results" presentationHeaderForVersions :: Namespace -> PackageName -> Word -> FloraHTML -presentationHeaderForVersions namespace packageName numberOfReleases = do - div_ [class_ "divider"] $ do - div_ [class_ "page-title"] $ do - h1_ [class_ ""] $ do - span_ [class_ "headline"] $ do - displayNamespace namespace - chevronRightOutline - linkToPackage namespace packageName - chevronRightOutline - toHtml (display Versions) - p_ [class_ "synopsis"] $ - span_ [class_ "version"] $ - toHtml $ - display numberOfReleases <> " results" +presentationHeaderForVersions namespace packageName numberOfReleases = div_ [class_ "divider"] $ do + div_ [class_ "page-title"] $ do + h1_ [class_ ""] $ do + span_ [class_ "headline"] $ do + displayNamespace namespace + chevronRightOutline + linkToPackage namespace packageName + chevronRightOutline + toHtml (display Versions) + p_ [class_ "synopsis"] $ + span_ [class_ "version"] $ + toHtml $ + display numberOfReleases <> " results" showDependents :: Namespace -> PackageName - -> Version + -> Release -> Word -> Vector DependencyInfo -> Positive Word -> FloraHTML -showDependents namespace packageName version count packagesInfo currentPage = +showDependents namespace packageName release count packagesInfo currentPage = div_ [class_ "container"] $ do - presentationHeaderForSubpage namespace packageName version Dependents count + presentationHeaderForSubpage namespace packageName release Dependents count div_ [class_ ""] $ do ul_ [class_ "package-list"] $ Vector.forM_ @@ -115,11 +113,11 @@ showDependents namespace packageName version count packagesInfo currentPage = when (count > 30) $ paginationNav count currentPage (DependentsOf namespace packageName) -showDependencies :: Namespace -> PackageName -> Version -> ComponentDependencies -> FloraHTML -showDependencies namespace packageName version componentsInfo = do +showDependencies :: Namespace -> PackageName -> Release -> ComponentDependencies -> FloraHTML +showDependencies namespace packageName release componentsInfo = do let dependenciesCount = fromIntegral $ Map.foldr (\v acc -> Vector.length v + acc) 0 componentsInfo div_ [class_ "container"] $ do - presentationHeaderForSubpage namespace packageName version Dependencies dependenciesCount + presentationHeaderForSubpage namespace packageName release Dependencies dependenciesCount div_ [class_ ""] $ requirementListing componentsInfo listVersions :: Namespace -> PackageName -> Vector Release -> FloraHTML @@ -130,8 +128,7 @@ listVersions namespace packageName releases = ul_ [class_ "package-list"] $ Vector.forM_ releases - ( \release -> do - versionListItem namespace packageName release + ( \release -> versionListItem namespace packageName release ) versionListItem :: Namespace -> PackageName -> Release -> FloraHTML @@ -157,8 +154,7 @@ packageListing packages = ul_ [class_ "package-list"] $ Vector.forM_ packages - ( \PackageInfo{..} -> do - packageListItem (namespace, name, synopsis, version, license) + ( \PackageInfo{..} -> packageListItem (namespace, name, synopsis, version, license) ) requirementListing :: ComponentDependencies -> FloraHTML @@ -166,18 +162,17 @@ requirementListing requirements = ul_ [class_ "component-list"] $ requirementListItem requirements showChangelog :: Namespace -> PackageName -> Version -> Maybe TextHtml -> FloraHTML -showChangelog namespace packageName version mChangelog = do - div_ [class_ "container"] $ do - div_ [class_ "divider"] $ do - div_ [class_ "page-title"] $ - h1_ [class_ ""] $ do - span_ [class_ "headline"] $ toHtml ("Changelog of " <> display namespace <> "/" <> display packageName) - toHtmlRaw @Text " " - span_ [class_ "version"] $ toHtml $ display version - section_ [class_ "release-changelog"] $ do - case mChangelog of - Nothing -> toHtml @Text "This release does not have a Changelog" - Just (MkTextHtml changelogText) -> relaxHtmlT changelogText +showChangelog namespace packageName version mChangelog = div_ [class_ "container"] $ do + div_ [class_ "divider"] $ do + div_ [class_ "page-title"] $ + h1_ [class_ ""] $ do + span_ [class_ "headline"] $ toHtml ("Changelog of " <> display namespace <> "/" <> display packageName) + toHtmlRaw @Text " " + span_ [class_ "version"] $ toHtml $ display version + section_ [class_ "release-changelog"] $ do + case mChangelog of + Nothing -> toHtml @Text "This release does not have a Changelog" + Just (MkTextHtml changelogText) -> relaxHtmlT changelogText displayReleaseVersion :: Version -> FloraHTML displayReleaseVersion = toHtml @@ -260,11 +255,11 @@ displayVersions namespace packageName versions numberOfReleases = displayVersion :: Release -> FloraHTML displayVersion release = li_ [class_ "release"] $ do - let versionClass = "release-version" <> if fromMaybe False release.deprecated then " release-deprecated instruction-tooltip" else "" - let dataText = ([dataText_ "This release is deprecated, pick another one" | fromMaybe False release.deprecated]) + let versionClass = "release-version" <> if Just True == release.deprecated then " release-deprecated instruction-tooltip" else "" + let dataText = ([dataText_ "This release is deprecated, pick another one" | Just True == release.deprecated]) a_ - ([class_ versionClass, href_ ("/" <> toUrlPiece (Links.packageVersionLink namespace packageName (release.version)))] <> dataText) - (toHtml $ display (release.version)) + ([class_ versionClass, href_ ("/" <> toUrlPiece (Links.packageVersionLink namespace packageName release.version))] <> dataText) + (toHtml $ display release.version) " " case release.uploadedAt of Nothing -> "" @@ -312,31 +307,31 @@ displayInstructions packageName latestRelease = displayPackageDeprecation :: PackageAlternatives -> FloraHTML displayPackageDeprecation (PackageAlternatives inFavourOf) = li_ [class_ ""] $ do - h3_ [class_ "package-body-section"] "Deprecated" - div_ [class_ "items-top"] $ div_ [class_ ""] $ do - if Vector.null inFavourOf - then label_ [for_ "install-string", class_ "font-light"] "This package has been deprecated" - else do - label_ [for_ "install-string", class_ "font-light"] "This package has been deprecated in favour of" - ul_ [class_ "package-alternatives"] $ - Vector.forM_ inFavourOf $ \PackageAlternative{namespace, package} -> - li_ [] $ - a_ - [href_ ("/packages/" <> display namespace <> "/" <> display package)] - (text $ display namespace <> "/" <> display package) + h3_ [class_ "package-body-section release-deprecated"] "Deprecated" + div_ [class_ "items-top"] $ + div_ [class_ ""] $ + if Vector.null inFavourOf + then label_ [for_ "install-string", class_ "font-light"] "This package has been deprecated" + else do + label_ [for_ "install-string", class_ "font-light"] "This package has been deprecated in favour of" + ul_ [class_ "package-alternatives"] $ + Vector.forM_ inFavourOf $ \PackageAlternative{namespace, package} -> + li_ [] $ + a_ + [href_ ("/packages/" <> display namespace <> "/" <> display package)] + (text $ display namespace <> "/" <> display package) displayReleaseDeprecation :: Maybe (Namespace, PackageName, Version) -> FloraHTML displayReleaseDeprecation mLatestViableRelease = li_ [class_ ""] $ do - h3_ [class_ "package-body-section"] "Deprecated" - div_ [class_ "items-top"] $ div_ [class_ ""] $ do - case mLatestViableRelease of - Nothing -> label_ [for_ "install-string", class_ "font-light"] "This release has been deprecated" - Just (namespace, package, version) -> do - label_ [for_ "install-string", class_ "font-light"] (text "This release has been deprecated in favour of: ") - a_ - [href_ ("/packages/" <> display namespace <> "/" <> display package <> "/" <> display version)] - (text $ display namespace <> "/" <> display package <> "-" <> display version) + h3_ [class_ "package-body-section release-deprecated"] "Deprecated" + div_ [class_ "items-top"] $ div_ [class_ ""] $ case mLatestViableRelease of + Nothing -> label_ [for_ "install-string", class_ "font-light"] "This release has been deprecated" + Just (namespace, package, version) -> do + label_ [for_ "install-string", class_ "font-light"] (text "This release has been deprecated in favour of: ") + a_ + [href_ ("/packages/" <> display namespace <> "/" <> display package <> "/" <> display version)] + (text $ display namespace <> "/" <> display package <> "-" <> display version) displayTestedWith :: Vector Version -> FloraHTML displayTestedWith compilersVersions' @@ -348,10 +343,7 @@ displayTestedWith compilersVersions' ul_ [class_ "compiler-badges"] $ Vector.forM_ compilersVersions - ( \version -> - li_ [] $ - a_ [class_ "compiler-badge"] $ - toHtml @Text (display version) + ( li_ [] . a_ [class_ "compiler-badge"] . toHtml @Text . display ) displayMaintainer :: Text -> FloraHTML @@ -429,14 +421,13 @@ displayPackageFlag MkPackageFlag{flagName, flagDescription, flagDefault} = case pre_ [class_ "package-flag-name"] (toHtml $ Text.pack (Flag.unFlagName flagName)) toHtmlRaw @Text " " defaultMarker flagDefault - _ -> do - details_ [] $ do - summary_ [] $ do - pre_ [class_ "package-flag-name"] (toHtml $ Text.pack (Flag.unFlagName flagName)) - toHtmlRaw @Text " " - defaultMarker flagDefault - div_ [class_ "package-flag-description"] $ do - renderHaddock $ Text.pack flagDescription + _ -> details_ [] $ do + summary_ [] $ do + pre_ [class_ "package-flag-name"] (toHtml $ Text.pack (Flag.unFlagName flagName)) + toHtmlRaw @Text " " + defaultMarker flagDefault + div_ [class_ "package-flag-description"] $ do + renderHaddock $ Text.pack flagDescription defaultMarker :: Bool -> FloraHTML defaultMarker True = em_ "(on by default)" diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs b/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs index 2571f34b..c6faf00b 100644 --- a/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs +++ b/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs @@ -55,7 +55,7 @@ showPackage numberOfDependencies categories = div_ [class_ "larger-container"] $ do - presentationHeader latestRelease namespace name (latestRelease.synopsis) + presentationHeader latestRelease namespace name latestRelease.synopsis packageBody package latestRelease @@ -70,13 +70,14 @@ showPackage presentationHeader :: Release -> Namespace -> PackageName -> Text -> FloraHTML presentationHeader release namespace name synopsis = div_ [class_ "divider"] $ do - div_ [class_ "page-title"] $ + div_ [class_ "page-title"] $ do h1_ [class_ "package-title"] $ do span_ [class_ "headline"] $ do displayNamespace namespace chevronRightOutline toHtml name - span_ [class_ "version"] $ displayReleaseVersion release.version + let versionClass = "version" <> if Just True == release.deprecated then " release-deprecated" else "" + span_ [class_ versionClass] $ displayReleaseVersion release.version div_ [class_ "synopsis"] $ p_ [class_ ""] (toHtml synopsis) @@ -121,7 +122,11 @@ packageBody displayDependents (namespace, packageName) numberOfDependents dependents displayPackageFlags flags -getLatestViableRelease :: Namespace -> PackageName -> Vector Release -> Maybe (Namespace, PackageName, Version) +getLatestViableRelease + :: Namespace + -> PackageName + -> Vector Release + -> Maybe (Namespace, PackageName, Version) getLatestViableRelease namespace packageName releases = releases & Vector.filter (\r -> not (fromMaybe False r.deprecated)) From c826afd8582d5668c778912933ee37d7eb61b1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sun, 1 Oct 2023 20:20:10 +0200 Subject: [PATCH 06/40] [FLORA-436] Store revision upload time separately (#437) --- CHANGELOG.md | 1 + Makefile | 2 + assets/css/3-screens/1-package/1-package.css | 34 +++++- assets/css/styles.css | 4 - flake.lock | 8 +- flora.cabal | 1 + migrations/20230924120348_add_revised_at.sql | 2 + src/core/Flora/Import/Package.hs | 30 ++--- src/core/Flora/Model/Job.hs | 2 + src/core/Flora/Model/Release/Types.hs | 5 +- src/core/Flora/Model/Release/Update.hs | 9 ++ src/jobs-worker/FloraJobs/Runner.hs | 34 +++--- .../FloraJobs/ThirdParties/Hackage/API.hs | 17 ++- .../FloraJobs/ThirdParties/Hackage/Client.hs | 30 ++++- src/web/FloraWeb/Pages/Server/Admin.hs | 24 ++-- src/web/FloraWeb/Pages/Templates/Packages.hs | 41 +++++-- ...splay.cabal => text-display-0.0.4.0.cabal} | 0 .../fixtures/Cabal/text-display-0.0.5.0.cabal | 105 ++++++++++++++++++ 18 files changed, 273 insertions(+), 76 deletions(-) create mode 100644 migrations/20230924120348_add_revised_at.sql rename test/fixtures/Cabal/{text-display.cabal => text-display-0.0.4.0.cabal} (100%) create mode 100644 test/fixtures/Cabal/text-display-0.0.5.0.cabal diff --git a/CHANGELOG.md b/CHANGELOG.md index eb7ebe02..6aa201ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ * Fix the margins of the search bar in mobile view ([#430](/~https://github.com/flora-pm/flora-server/pull/430)) * Have proper breadcrumbs for the package page title ([#431](/~https://github.com/flora-pm/flora-server/pull/431)) * Configure the API gateway ([#432](/~https://github.com/flora-pm/flora-server/pull/432)) +* Store and show the latest revision date of releases ([#437](/~https://github.com/flora-pm/flora-server/pull/437)) ## 1.0.12 -- 2023-04-04 diff --git a/Makefile b/Makefile index 1414b447..f42556d2 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,8 @@ db-reset: db-drop db-setup db-provision ## Reset the dev database db-provision: build ## Load the development data in the database @cabal run -- flora-cli create-user --username "hackage-user" --email "tech@flora.pm" --password "foobar2000" @cabal run -- flora-cli provision categories + +db-provision-test-packages: @cabal run -- flora-cli provision test-packages import-from-hackage: ## Imports every cabal file from the ./index-01 directory diff --git a/assets/css/3-screens/1-package/1-package.css b/assets/css/3-screens/1-package/1-package.css index 4a04a57f..71bf741d 100644 --- a/assets/css/3-screens/1-package/1-package.css +++ b/assets/css/3-screens/1-package/1-package.css @@ -119,6 +119,39 @@ } } +.release { + a:hover { + text-decoration: underline; + } +} + +span.revised-date::before { + content: attr(data-text); /* here's the magic */ + position: absolute; + font-size: 0.85em; + + /* vertically center */ + top: 50%; + transform: translateY(-50%); + + /* move to right */ + left: 100%; + + /* basic styles */ + background: #000; + border-radius: 10px; + box-shadow: 0 1px 8px rgb(0 0 0 / 50%); + color: #fff; + padding: 5px; + text-align: center; + width: 200px; + display: none; /* hide by default */ +} + +span.revised-date:hover::before { + display: block; +} + .instruction-tooltip { svg { display: inline; @@ -140,7 +173,6 @@ /* move to right */ left: 100%; - margin-left: 15px; /* and add a small left margin */ /* basic styles */ background: #000; diff --git a/assets/css/styles.css b/assets/css/styles.css index caabebeb..7825d9f8 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -149,10 +149,6 @@ } } -.release a:hover { - text-decoration: underline; -} - .theme-button--light { display: none; } diff --git a/flake.lock b/flake.lock index 24bbb448..adc96ef1 100644 --- a/flake.lock +++ b/flake.lock @@ -201,11 +201,11 @@ "nixpkgs": "nixpkgs_4" }, "locked": { - "lastModified": 1689157215, - "narHash": "sha256-RbmIc0+wcoZzQ6MBXsFcNhOJts3yWmBrbHnZcr5hhsY=", + "lastModified": 1696069899, + "narHash": "sha256-ZLIgeaIjmzlICtCg88xiaJsM7u03F3WGxoDCAknCfvE=", "ref": "refs/heads/master", - "rev": "f586739656a9ac84528f09dcf2aebe48d26699d2", - "revCount": 1126, + "rev": "a863ad211af049462319295cb74f0261c88532ca", + "revCount": 1130, "type": "git", "url": "https://gitlab.horizon-haskell.net/package-sets/horizon-platform" }, diff --git a/flora.cabal b/flora.cabal index 21432020..9ac1d7c9 100644 --- a/flora.cabal +++ b/flora.cabal @@ -353,6 +353,7 @@ library flora-jobs , pg-entity , pg-transact-effectful , postgresql-simple + , req , resource-pool , servant , servant-client diff --git a/migrations/20230924120348_add_revised_at.sql b/migrations/20230924120348_add_revised_at.sql new file mode 100644 index 00000000..e8a038b6 --- /dev/null +++ b/migrations/20230924120348_add_revised_at.sql @@ -0,0 +1,2 @@ +alter table releases + add revised_at timestamptz diff --git a/src/core/Flora/Import/Package.hs b/src/core/Flora/Import/Package.hs index b7a298d8..a854f608 100644 --- a/src/core/Flora/Import/Package.hs +++ b/src/core/Flora/Import/Package.hs @@ -237,7 +237,7 @@ loadAndExtractCabalFile userId filePath = persistImportOutput :: (DB :> es, IOE :> es) => Poolboy.WorkQueue -> ImportOutput -> Eff es () persistImportOutput wq (ImportOutput package categories release components) = do dbPool <- getPool - liftIO . T.putStrLn $ "📦 Persisting package: " <> packageName <> ", 🗓 Release v" <> display (release.version) + liftIO . T.putStrLn $ "📦 Persisting package: " <> packageName <> ", 🗓 Release v" <> display release.version persistPackage Update.upsertRelease release parallelRun dbPool (persistComponent dbPool) components @@ -245,7 +245,7 @@ persistImportOutput wq (ImportOutput package categories release components) = do where parallelRun :: (MonadIO m, Foldable t) => Pool Connection -> (a -> Eff [DB, IOE] b) -> t a -> m () parallelRun pool f xs = liftIO $ forM_ xs $ Poolboy.enqueue wq . void . runEff . runDB pool . f - packageName = display (package.namespace) <> "/" <> display (package.name) + packageName = display package.namespace <> "/" <> display package.name persistPackage = do let packageId = package.packageId Update.upsertPackage package @@ -254,7 +254,7 @@ persistImportOutput wq (ImportOutput package categories release components) = do persistComponent dbPool (packageComponent, deps) = do liftIO . T.putStrLn $ "🧩 Persisting component: " - <> display (packageComponent.canonicalForm) + <> display packageComponent.canonicalForm <> " with " <> display (length deps) <> " dependencies." @@ -262,8 +262,8 @@ persistImportOutput wq (ImportOutput package categories release components) = do parallelRun dbPool persistImportDependency deps persistImportDependency dep = do - Update.upsertPackage (dep.package) - Update.upsertRequirement (dep.requirement) + Update.upsertPackage dep.package + Update.upsertRequirement dep.requirement withWorkerDbPool :: (Reader PoolConfig :> es, IOE :> es) => (Poolboy.WorkQueue -> Eff es a) -> Eff es a withWorkerDbPool f = do @@ -280,12 +280,12 @@ extractPackageDataFromCabal userId repository uploadTime genericDesc = do let packageDesc = genericDesc.packageDescription let flags = Vector.fromList genericDesc.genPackageFlags let packageName = force $ packageDesc ^. #package % #pkgName % to unPackageName % to pack % to PackageName - let packageVersion = force $ packageDesc.package.pkgVersion + let packageVersion = force packageDesc.package.pkgVersion let namespace = force $ chooseNamespace packageName let packageId = force $ deterministicPackageId namespace packageName let releaseId = force $ deterministicReleaseId packageId packageVersion timestamp <- Time.currentTime - let sourceRepos = getRepoURL packageName $ packageDesc.sourceRepos + let sourceRepos = getRepoURL packageName packageDesc.sourceRepos let rawCategoryField = packageDesc ^. #category % to Cabal.fromShortText % to T.pack let categoryList = fmap (Tuning.UserPackageCategory . T.stripStart . T.stripEnd) (T.splitOn "," rawCategoryField) categories <- liftIO $ Tuning.normalisedCategories <$> Tuning.normalise categoryList @@ -326,10 +326,11 @@ extractPackageDataFromCabal userId repository uploadTime genericDesc = do , flags = ReleaseFlags flags , testedWith = getVersions . extractTestedWith . Vector.fromList $ packageDesc.testedWith , deprecated = Nothing + , revisedAt = Nothing } let lib = extractLibrary package release Nothing [] <$> allLibraries packageDesc - let condLib = maybe [] (extractCondTree extractLibrary package release Nothing) (genericDesc.condLibrary) + let condLib = maybe [] (extractCondTree extractLibrary package release Nothing) genericDesc.condLibrary let condSubLibs = extractCondTrees extractLibrary package release genericDesc.condSubLibraries let foreignLibs = extractForeignLib package release Nothing [] <$> packageDesc.foreignLibs @@ -367,16 +368,15 @@ extractLibrary package = package where getLibName :: LibraryName -> Text - getLibName LMainLibName = display (package.name) + getLibName LMainLibName = display package.name getLibName (LSubLibName lname) = T.pack $ unUnqualComponentName lname extractForeignLib :: Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> ForeignLib -> ImportComponent -extractForeignLib package = +extractForeignLib = genericComponentExtractor Component.ForeignLib (^. #foreignLibName % to unUnqualComponentName % to T.pack) (^. #foreignLibBuildInfo % #targetBuildDepends) - package extractExecutable :: Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> Executable -> ImportComponent extractExecutable = @@ -411,8 +411,8 @@ extractCondTree extractCondTree extractor package release defaultComponentName = go [] where go cond tree = - let treeComponent = extractor package release defaultComponentName cond $ tree.condTreeData - treeSubComponents = (tree.condTreeComponents) >>= extractBranch + let treeComponent = extractor package release defaultComponentName cond tree.condTreeData + treeSubComponents = tree.condTreeComponents >>= extractBranch in treeComponent : treeSubComponents extractBranch CondBranch{condBranchCondition, condBranchIfTrue, condBranchIfFalse} = let condIfTrueComponents = go [condBranchCondition] condBranchIfTrue @@ -486,7 +486,7 @@ buildDependency package packageComponentId (Cabal.Dependency depName versionRang getRepoURL :: PackageName -> [Cabal.SourceRepo] -> Vector Text getRepoURL _ [] = Vector.empty -getRepoURL _ (repo : _) = Vector.singleton $ display $ fromMaybe mempty (repo.repoLocation) +getRepoURL _ (repo : _) = Vector.singleton $ display $ fromMaybe mempty repo.repoLocation chooseNamespace :: PackageName -> Namespace chooseNamespace name | Set.member name coreLibraries = Namespace "haskell" @@ -502,7 +502,7 @@ extractTestedWith testedWithVector = getVersions :: Vector VersionRange -> Vector Version getVersions supportedCompilers = foldMap - (\version -> Vector.foldMap (\versionRange -> checkVersion version versionRange) supportedCompilers) + (\version -> Vector.foldMap (checkVersion version) supportedCompilers) versionList checkVersion :: Version -> VersionRange -> Vector Version diff --git a/src/core/Flora/Model/Job.hs b/src/core/Flora/Model/Job.hs index 1d8ce000..2774c6dd 100644 --- a/src/core/Flora/Model/Job.hs +++ b/src/core/Flora/Model/Job.hs @@ -95,4 +95,6 @@ instance ToJSON LogEvent where LogJobTimeout job -> toJSON ("timed-out" :: Text, job) LogPoll -> toJSON ("poll" :: Text) LogWebUIRequest -> toJSON ("web-ui-request" :: Text) + LogKillJobSuccess job -> toJSON ("kill-success" :: Text, job) + LogKillJobFailed job -> toJSON ("kill-failed" :: Text, job) LogText other -> toJSON ("other" :: Text, other) diff --git a/src/core/Flora/Model/Release/Types.hs b/src/core/Flora/Model/Release/Types.hs index 06f0a48d..67a01f96 100644 --- a/src/core/Flora/Model/Release/Types.hs +++ b/src/core/Flora/Model/Release/Types.hs @@ -61,7 +61,7 @@ instance ToJSON TextHtml where toJSON (MkTextHtml a) = String $ Text.toStrict $ Lucid.renderText a instance FromJSON TextHtml where - parseJSON = withText "TextHtml" (\text -> pure $ MkTextHtml $ Lucid.toHtmlRaw @Text text) + parseJSON = withText "TextHtml" (pure . MkTextHtml . Lucid.toHtmlRaw @Text) instance NFData TextHtml where rnf a = seq a () @@ -100,6 +100,7 @@ data Release = Release , testedWith :: Vector Version , deprecated :: Maybe Bool , repository :: Maybe Text + , revisedAt :: Maybe UTCTime } deriving stock (Eq, Show, Generic) deriving anyclass (FromRow, ToRow, NFData) @@ -108,7 +109,7 @@ data Release = Release via (GenericEntity '[TableName "releases"] Release) instance Ord Release where - compare x y = compare (x.version) (y.version) + compare x y = compare x.version y.version newtype ReleaseFlags = ReleaseFlags (Vector PackageFlag) deriving stock (Eq, Ord, Show, Generic) diff --git a/src/core/Flora/Model/Release/Update.hs b/src/core/Flora/Model/Release/Update.hs index fbb17b89..d716c494 100644 --- a/src/core/Flora/Model/Release/Update.hs +++ b/src/core/Flora/Model/Release/Update.hs @@ -47,6 +47,15 @@ updateUploadTime releaseId timestamp = ([field| release_id |], releaseId) (Only (Just timestamp)) +updateRevisionTime :: DB :> es => ReleaseId -> UTCTime -> Eff es () +updateRevisionTime releaseId timestamp = + dbtToEff $ + void $ + updateFieldsBy @Release + [[field| revised_at |]] + ([field| release_id |], releaseId) + (Only (Just timestamp)) + updateChangelog :: DB :> es => ReleaseId -> Maybe TextHtml -> ImportStatus -> Eff es () updateChangelog releaseId changelogBody status = dbtToEff $ diff --git a/src/jobs-worker/FloraJobs/Runner.hs b/src/jobs-worker/FloraJobs/Runner.hs index ac7481e6..6fab1422 100644 --- a/src/jobs-worker/FloraJobs/Runner.hs +++ b/src/jobs-worker/FloraJobs/Runner.hs @@ -8,7 +8,6 @@ import Data.Aeson (Result (..), fromJSON, toJSON) import Data.Function import Data.Set qualified as Set import Data.Text.Display -import Data.Text.Lazy.Encoding qualified as TL import Data.Vector (Vector) import Data.Vector qualified as Vector import Effectful.PostgreSQL.Transact.Effect @@ -28,7 +27,7 @@ import Flora.Model.Release.Types import Flora.Model.Release.Update qualified as Update import FloraJobs.Render (renderMarkdown) import FloraJobs.Scheduler -import FloraJobs.ThirdParties.Hackage.API (HackagePreferredVersions (..), VersionedPackage (..)) +import FloraJobs.ThirdParties.Hackage.API (HackagePackageInfo (..), HackagePreferredVersions (..), VersionedPackage (..)) import FloraJobs.ThirdParties.Hackage.Client qualified as Hackage import FloraJobs.Types @@ -110,23 +109,16 @@ fetchUploadTime payload@UploadTimeJobPayload{packageName, packageVersion, releas localDomain "fetch-upload-time" $ do logInfo "Fetching upload time" payload let requestPayload = VersionedPackage packageName packageVersion - result <- Hackage.request $ Hackage.getPackageUploadTime requestPayload - case result of - Right timestamp -> do - logInfo_ $ "Got a timestamp for " <> display packageName - Update.updateUploadTime releaseId timestamp - Left e@(FailureResponse _ response) - -- If the upload time simply doesn't exist, we skip it by marking the job as successful. - | response.responseStatusCode == notFound404 -> pure () - | response.responseStatusCode == gone410 -> pure () - | otherwise -> do - logAttention "Timestamp retrieval failed" $ - object - [ "status" .= statusCode (response.responseStatusCode) - , "body" .= TL.decodeUtf8 (response.responseBody) - ] - throw e - Left e -> throw e + packageInfo <- liftIO $ Hackage.getPackageInfo requestPayload + if packageInfo.metadataRevision == 0 + then do + Log.logInfo_ "No revision, using the upload time" + Update.updateUploadTime releaseId packageInfo.uploadedAt + else do + Log.logInfo_ "Found a revision, querying the original package info" + originalPackageInfo <- liftIO $ Hackage.getPackageWithRevision requestPayload 0 + Update.updateRevisionTime releaseId packageInfo.uploadedAt + Update.updateUploadTime releaseId originalPackageInfo.uploadedAt -- | This job fetches the deprecation list and inserts the appropriate metadata in the packages fetchPackageDeprecationList :: JobsRunner () @@ -144,7 +136,7 @@ fetchPackageDeprecationList = do Left e@(FailureResponse _ response) -> do logAttention "Could not fetch package deprecation list from Hackage" $ object - [ "status_code" .= statusCode (response.responseStatusCode) + [ "status_code" .= statusCode response.responseStatusCode ] throw e Left e -> throw e @@ -175,7 +167,7 @@ fetchReleaseDeprecationList packageName releases = do logAttention "Could not fetch release deprecation list from Hackage" $ object [ "package" .= display packageName - , "status_code" .= statusCode (response.responseStatusCode) + , "status_code" .= statusCode response.responseStatusCode ] throw e Left e -> throw e diff --git a/src/jobs-worker/FloraJobs/ThirdParties/Hackage/API.hs b/src/jobs-worker/FloraJobs/ThirdParties/Hackage/API.hs index 378e9eee..8aea200d 100644 --- a/src/jobs-worker/FloraJobs/ThirdParties/Hackage/API.hs +++ b/src/jobs-worker/FloraJobs/ThirdParties/Hackage/API.hs @@ -1,6 +1,9 @@ +{-# LANGUAGE TemplateHaskell #-} + module FloraJobs.ThirdParties.Hackage.API where import Data.Aeson +import Data.Aeson.TH import Data.Bifunctor qualified as Bifunctor import Data.ByteString.Lazy as ByteString import Data.List.NonEmpty @@ -46,7 +49,7 @@ data HackageAPI' mode = HackageAPI' , withUser :: mode :- "user" :> Capture "username" Text :> NamedRoutes HackageUserAPI , packages :: mode :- "packages" :> NamedRoutes HackagePackagesAPI , withPackage :: mode :- "package" :> Capture "versioned_package" VersionedPackage :> NamedRoutes HackagePackageAPI - , withPackageName :: mode :- "package" :> Capture "pacakgeName" PackageName :> NamedRoutes HackagePackageAPI + , withPackageNameOnly :: mode :- "package" :> Capture "packageName" PackageName :> NamedRoutes HackagePackageAPI } deriving stock (Generic) @@ -60,6 +63,8 @@ data HackagePackageAPI mode = HackagePackageAPI , getUploadTime :: mode :- "upload-time" :> Get '[PlainText] UTCTime , getChangelog :: mode :- "changelog.txt" :> Get '[PlainerText] Text , getDeprecatedReleases :: mode :- "preferred.json" :> Get '[JSON] HackagePreferredVersions + , getPackageInfo :: mode :- Get '[JSON] HackagePackageInfo + , getPackageWithRevision :: mode :- "revision" :> Capture "revision_number" Text :> Get '[JSON] HackagePackageInfo } deriving stock (Generic) @@ -90,7 +95,15 @@ data HackagePreferredVersions = HackagePreferredVersions deriving stock (Eq, Show, Generic) instance FromJSON HackagePreferredVersions where - parseJSON = withObject "Hacakge preferred versions" $ \o -> do + parseJSON = withObject "Hackage preferred versions" $ \o -> do deprecatedVersions <- o .:? "deprecated-version" .!= Vector.empty normalVersions <- o .: "normal-version" pure HackagePreferredVersions{..} + +data HackagePackageInfo = HackagePackageInfo + { metadataRevision :: Word + , uploadedAt :: UTCTime + } + deriving stock (Eq, Show) + +$(deriveJSON defaultOptions{fieldLabelModifier = camelTo2 '_'} ''HackagePackageInfo) diff --git a/src/jobs-worker/FloraJobs/ThirdParties/Hackage/Client.hs b/src/jobs-worker/FloraJobs/ThirdParties/Hackage/Client.hs index e3f9b12d..7ba1e69f 100644 --- a/src/jobs-worker/FloraJobs/ThirdParties/Hackage/Client.hs +++ b/src/jobs-worker/FloraJobs/ThirdParties/Hackage/Client.hs @@ -10,8 +10,10 @@ import Data.Time (UTCTime) import Data.Time.Orphans () import Data.Vector (Vector) import Effectful.Reader.Static +import Network.HTTP.Req (GET (GET), NoReqBody (..)) +import Network.HTTP.Req qualified as Req import Servant.API () -import Servant.Client +import Servant.Client (BaseUrl (..), Client, ClientError (..), ClientM, Scheme (..), client, mkClientEnv, runClientM, (//), (/:)) import Flora.Model.Package.Types import FloraJobs.ThirdParties.Hackage.API as API @@ -65,6 +67,30 @@ getDeprecatedPackages = getDeprecatedReleasesList :: PackageName -> ClientM HackagePreferredVersions getDeprecatedReleasesList packageName = hackageClient - // API.withPackageName + // API.withPackageNameOnly /: packageName // getDeprecatedReleases + +getPackageInfo :: VersionedPackage -> IO HackagePackageInfo +getPackageInfo versionedPackage = do + Req.runReq Req.defaultHttpConfig $ do + response <- + Req.req + GET + (Req.https "hackage.haskell.org" Req./: "package" Req./~ versionedPackage) + NoReqBody + Req.jsonResponse + mempty + pure $ Req.responseBody response + +getPackageWithRevision :: VersionedPackage -> Word -> IO HackagePackageInfo +getPackageWithRevision versionedPackage revision = do + Req.runReq Req.defaultHttpConfig $ do + response <- + Req.req + GET + (Req.https "hackage.haskell.org" Req./: "package" Req./~ versionedPackage Req./: "revision" Req./~ revision) + NoReqBody + Req.jsonResponse + mempty + pure $ Req.responseBody response diff --git a/src/web/FloraWeb/Pages/Server/Admin.hs b/src/web/FloraWeb/Pages/Server/Admin.hs index 23cfbe89..d5a260ae 100644 --- a/src/web/FloraWeb/Pages/Server/Admin.hs +++ b/src/web/FloraWeb/Pages/Server/Admin.hs @@ -47,8 +47,7 @@ server cfg env = -- to a sub-tree of Flora pages. -- It acts as the safeguard that rejects non-admins from protected routes. ensureAdmin :: ServerT Routes FloraAdmin -> ServerT Routes FloraPage -ensureAdmin adminServer = do - hoistServer (Proxy :: Proxy Routes) checkAdmin adminServer +ensureAdmin adminServer = hoistServer (Proxy :: Proxy Routes) checkAdmin adminServer where checkAdmin :: FloraAdmin a -> FloraPage a checkAdmin adminRoutes = do @@ -65,14 +64,14 @@ indexHandler = do templateEnv <- fromSession session defaultTemplateEnv >>= \te -> pure $ set (#activeElements % #adminDashboard) True te - FloraEnv{pool} <- liftIO $ fetchFloraEnv (session.webEnvStore) + FloraEnv{pool} <- liftIO $ fetchFloraEnv session.webEnvStore report <- liftIO $ withPool pool getReport render templateEnv (Templates.index report) fetchMetadataHandler :: FloraAdmin FetchMetadataResponse fetchMetadataHandler = do session <- getSession - FloraEnv{jobsPool} <- liftIO $ fetchFloraEnv (session.webEnvStore) + FloraEnv{jobsPool} <- liftIO $ fetchFloraEnv session.webEnvStore liftIO $ void $ schedulePackageDeprecationListJob jobsPool @@ -82,8 +81,7 @@ fetchMetadataHandler = do forkIO $ Async.forConcurrently_ releasesWithoutReadme - ( \(releaseId, version, packagename) -> do - scheduleReadmeJob jobsPool releaseId packagename version + ( \(releaseId, version, packagename) -> scheduleReadmeJob jobsPool releaseId packagename version ) releasesWithoutUploadTime <- Query.getPackageReleasesWithoutUploadTimestamp @@ -92,8 +90,7 @@ fetchMetadataHandler = do forkIO $ Async.forConcurrently_ releasesWithoutUploadTime - ( \(releaseId, version, packagename) -> do - scheduleUploadTimeJob jobsPool releaseId packagename version + ( \(releaseId, version, packagename) -> scheduleUploadTimeJob jobsPool releaseId packagename version ) releasesWithoutChangelog <- Query.getPackageReleasesWithoutChangelog @@ -102,8 +99,7 @@ fetchMetadataHandler = do forkIO $ Async.forConcurrently_ releasesWithoutChangelog - ( \(releaseId, version, packagename) -> do - scheduleChangelogJob jobsPool releaseId packagename version + ( \(releaseId, version, packagename) -> scheduleChangelogJob jobsPool releaseId packagename version ) packagesWithoutDeprecationInformation <- Query.getPackagesWithoutReleaseDeprecationInformation @@ -112,8 +108,7 @@ fetchMetadataHandler = do forkIO $ do Async.forConcurrently_ packagesWithoutDeprecationInformation - ( \a -> do - scheduleReleaseDeprecationListJob jobsPool a + ( \a -> scheduleReleaseDeprecationListJob jobsPool a ) void $ scheduleRefreshLatestVersions jobsPool @@ -122,7 +117,7 @@ fetchMetadataHandler = do indexImportJobHandler :: FloraAdmin ImportIndexResponse indexImportJobHandler = do session <- getSession - FloraEnv{jobsPool} <- liftIO $ fetchFloraEnv (session.webEnvStore) + FloraEnv{jobsPool} <- liftIO $ fetchFloraEnv session.webEnvStore liftIO $ void $ scheduleIndexImportJob jobsPool pure $ redirect "/admin" @@ -153,8 +148,7 @@ showUserHandler userId = do templateEnv <- fromSession session defaultTemplateEnv case result of Nothing -> renderError templateEnv notFound404 - Just user -> do - render templateEnv (Templates.showUser user) + Just user -> render templateEnv (Templates.showUser user) adminPackagesHandler :: ServerT PackagesAdminRoutes FloraAdmin adminPackagesHandler = diff --git a/src/web/FloraWeb/Pages/Templates/Packages.hs b/src/web/FloraWeb/Pages/Templates/Packages.hs index e24a4b99..1ae8a44b 100644 --- a/src/web/FloraWeb/Pages/Templates/Packages.hs +++ b/src/web/FloraWeb/Pages/Templates/Packages.hs @@ -65,10 +65,10 @@ presentationHeaderForSubpage namespace packageName release target numberOfPackag span_ [class_ "headline"] $ do displayNamespace namespace chevronRightOutline - linkToPackageWithVersion namespace packageName release.version + linkToPackageWithVersion namespace packageName (release.version) chevronRightOutline toHtml (display target) - p_ [class_ "synopsis"] $ do + p_ [class_ "synopsis"] $ span_ [class_ "version"] $ toHtml $ display numberOfPackages <> " results" @@ -144,9 +144,10 @@ versionListItem namespace packageName release = do strong_ [class_ ""] . toHtml $ "v" <> toHtml release.version uploadedAt - div_ [class_ "package-list-item__metadata"] $ span_ [class_ "package-list-item__license"] $ do - licenseIcon - toHtml release.license + div_ [class_ "package-list-item__metadata"] $ + span_ [class_ "package-list-item__license"] $ do + licenseIcon + toHtml release.license -- | Render a list of package informations packageListing :: Vector PackageInfo -> FloraHTML @@ -255,7 +256,7 @@ displayVersions namespace packageName versions numberOfReleases = displayVersion :: Release -> FloraHTML displayVersion release = li_ [class_ "release"] $ do - let versionClass = "release-version" <> if Just True == release.deprecated then " release-deprecated instruction-tooltip" else "" + let versionClass = "release-version" <> if Just True == release.deprecated then " release-deprecated" else "" let dataText = ([dataText_ "This release is deprecated, pick another one" | Just True == release.deprecated]) a_ ([class_ versionClass, href_ ("/" <> toUrlPiece (Links.packageVersionLink namespace packageName release.version))] <> dataText) @@ -264,7 +265,18 @@ displayVersions namespace packageName versions numberOfReleases = case release.uploadedAt of Nothing -> "" Just ts -> - span_ [] (toHtml $ Time.formatTime defaultTimeLocale "%a, %_d %b %Y" ts) + span_ [] $ do + toHtml $ Time.formatTime defaultTimeLocale "%a, %_d %b %Y" ts + case release.revisedAt of + Nothing -> do + span_ [] "" + Just revisionDate -> do + span_ + [ dataText_ + ("Revised on " <> display (Time.formatTime defaultTimeLocale "%a, %_d %b %Y, %R %EZ" revisionDate)) + , class_ "revised-date" + ] + pen displayDependencies :: (Namespace, PackageName, Version) @@ -325,7 +337,7 @@ displayReleaseDeprecation :: Maybe (Namespace, PackageName, Version) -> FloraHTM displayReleaseDeprecation mLatestViableRelease = li_ [class_ ""] $ do h3_ [class_ "package-body-section release-deprecated"] "Deprecated" - div_ [class_ "items-top"] $ div_ [class_ ""] $ case mLatestViableRelease of + div_ [class_ "items-top"] $ case mLatestViableRelease of Nothing -> label_ [for_ "install-string", class_ "font-light"] "This release has been deprecated" Just (namespace, package, version) -> do label_ [for_ "install-string", class_ "font-light"] (text "This release has been deprecated in favour of: ") @@ -439,7 +451,7 @@ usageInstructionTooltip :: FloraHTML usageInstructionTooltip = toHtmlRaw @Text [str| - + |] @@ -448,11 +460,20 @@ chevronRightOutline :: FloraHTML chevronRightOutline = toHtmlRaw @Text [str| - + |] +pen :: FloraHTML +pen = + toHtmlRaw @Text + [str| + + + +|] + -- | @datalist@ element dataText_ :: Text -> Attribute dataText_ = makeAttribute "data-text" diff --git a/test/fixtures/Cabal/text-display.cabal b/test/fixtures/Cabal/text-display-0.0.4.0.cabal similarity index 100% rename from test/fixtures/Cabal/text-display.cabal rename to test/fixtures/Cabal/text-display-0.0.4.0.cabal diff --git a/test/fixtures/Cabal/text-display-0.0.5.0.cabal b/test/fixtures/Cabal/text-display-0.0.5.0.cabal new file mode 100644 index 00000000..1c522c8a --- /dev/null +++ b/test/fixtures/Cabal/text-display-0.0.5.0.cabal @@ -0,0 +1,105 @@ +cabal-version: 3.0 +name: text-display +version: 0.0.5.0 +x-revision: 1 +category: Text +synopsis: A typeclass for user-facing output +description: + The 'Display' typeclass provides a solution for user-facing output that does not have to abide by the rules of the Show typeclass. + +homepage: + https://hackage.haskell.org/package/text-display-0.0.5.0/docs/doc/book/Introduction.html + +bug-reports: /~https://github.com/haskell-text/text-display/issues +author: Hécate Moonlight +maintainer: Hécate Moonlight +license: MIT +build-type: Simple +tested-with: + GHC ==8.8.4 || ==8.10.7 || ==9.0.2 || ==9.2.7 || ==9.4.5 || ==9.6.1 + +extra-source-files: + LICENSE + README.md + +extra-doc-files: + ./doc/book/*.css + ./doc/book/*.html + ./doc/book/*.js + ./doc/book/css/*.css + ./doc/book/favicon.png + ./doc/book/favicon.svg + ./doc/book/FontAwesome/css/font-awesome.css + ./doc/book/FontAwesome/fonts/fontawesome-webfont.woff2 + ./doc/book/fonts/*.css + ./doc/book/fonts/*.woff2 + ./doc/book/searchindex.json + CHANGELOG.md + +flag book + description: Enable the generation of the book + default: False + manual: True + +source-repository head + type: git + location: /~https://github.com/haskell-text/text-display + +common common-extensions + default-language: Haskell2010 + +common common-ghc-options + ghc-options: + -Wall -Wcompat -Widentities -Wincomplete-record-updates + -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints + -fhide-source-paths -Wno-unused-do-bind -funbox-strict-fields + -Wunused-packages + +common common-rts-options + ghc-options: -rtsopts -threaded -with-rtsopts=-N + +library + import: common-extensions + import: common-ghc-options + hs-source-dirs: src + exposed-modules: + Data.Text.Display + Data.Text.Display.Core + Data.Text.Display.Generic + build-depends: + , base >=4.12 && <5.0 + , bytestring >=0.10 && <0.12 + , text >=2.0 + +executable book + import: common-extensions + import: common-ghc-options + import: common-rts-options + main-is: Main.hs + hs-source-dirs: doc/src + + if !flag(book) + buildable: False + + build-depends: + , base + , directory + , literatex + , shake + +test-suite text-display-test + import: common-extensions + import: common-ghc-options + import: common-rts-options + type: exitcode-stdio-1.0 + main-is: Main.hs + hs-source-dirs: test + build-depends: + , base + , deepseq + , quickcheck-text + , tasty + , tasty-hunit + , tasty-quickcheck + , text + , text-display From d23586f629c35cb6ae94f90a9075145c0b0140b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Mon, 2 Oct 2023 18:13:33 +0200 Subject: [PATCH 07/40] [NO-ISSUE] Use haskell-action/setup (#441) --- .github/workflows/backend.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index e0ebcd97..3f2a8db2 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -52,7 +52,7 @@ jobs: - name: Set up Haskell id: setup-haskell - uses: haskell/actions/setup@v2 + uses: haskell-actions/setup@v2 with: ghc-version: "${{ matrix.ghc }}" cabal-version: "latest" @@ -83,7 +83,8 @@ jobs: uses: actions/cache@v3.3.2 with: path: ${{ steps.setup-haskell.outputs.cabal-store }} - key: ghc-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }} + key: ${{ runner.os }}-ghc-${{ matrix.ghc }}-cabal-${{ hashFiles('**/plan.json') }} + restore-keys: ${{ runner.os }}-ghc-${{ matrix.ghc }}- - name: Build run: | From 8ab893527b04d8ea429b41dc0e62cfaa10de7ad4 Mon Sep 17 00:00:00 2001 From: Lemon <55360995+LemonjamesD@users.noreply.github.com> Date: Tue, 3 Oct 2023 07:17:05 -0400 Subject: [PATCH 08/40] [NO-ISSUE] Added more Natural Language categories (#440) --- CHANGELOG.md | 2 +- cbits/categorise.dl | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aa201ca..bd4c7cd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # CHANGELOG ## 1.0.14 -- XXXX-XX-XX - +* Added more matches to the the natural language processing catergory ([#44](/~https://github.com/flora-pm/flora-server/pull/440)) * Colourise in red deprecation markers on the package page ([#438](/~https://github.com/flora-pm/flora-server/pull/439)) ## 1.0.13 -- 2023-09-17 diff --git a/cbits/categorise.dl b/cbits/categorise.dl index c8584fe0..a02b01cf 100644 --- a/cbits/categorise.dl +++ b/cbits/categorise.dl @@ -181,7 +181,11 @@ normalise_category("Database", "Databases"). normalise_category("PostgreSQL", "Databases"). normalise_category("NLP", "Natural Language Processing"). +normalise_category("Japanese Natural Language Processing", "Natural Language Processing"). +normalise_category("Natural Language", "Natural Language Processing"). +normalise_category("Natural Language Processing", "Natural Language Processing"). normalise_category("Stemming", "Natural Language Processing"). +normalise_category("Natural-language-processing", "Natural Language Processing"). normalise_category("Containers", "Data Structures"). From c684f7c05e699610a2233f873fc3cfd55cff31cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:14:29 +0200 Subject: [PATCH 09/40] Bump postcss from 8.4.25 to 8.4.31 in /assets (#445) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- assets/package.json | 2 +- assets/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/package.json b/assets/package.json index 5bad6d99..409d32ae 100644 --- a/assets/package.json +++ b/assets/package.json @@ -15,7 +15,7 @@ "esbuild-copy-static-files": "^0.1.0", "esbuild-plugin-assets-manifest": "^1.0.7", "esbuild-style-plugin": "^1.6.0", - "postcss": "^8.4.20", + "postcss": "^8.4.31", "postcss-cli": "^9.0.2", "postcss-copy": "^7.1.0", "postcss-hash": "^3.0.0", diff --git a/assets/yarn.lock b/assets/yarn.lock index b3f03417..36942fb1 100644 --- a/assets/yarn.lock +++ b/assets/yarn.lock @@ -2450,10 +2450,10 @@ postcss@^6.0.3: source-map "^0.6.1" supports-color "^5.4.0" -postcss@^8.0.9, postcss@^8.2.4, postcss@^8.3.5, postcss@^8.4.12, postcss@^8.4.20, postcss@^8.4.24: - version "8.4.25" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.25.tgz#4a133f5e379eda7f61e906c3b1aaa9b81292726f" - integrity sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw== +postcss@^8.0.9, postcss@^8.2.4, postcss@^8.3.5, postcss@^8.4.12, postcss@^8.4.24, postcss@^8.4.31: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: nanoid "^3.3.6" picocolors "^1.0.0" From b10c1cfa73e25922fe22bb35e81f9c7c9b570fe9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:23:29 +0200 Subject: [PATCH 10/40] Bump postcss from 8.4.27 to 8.4.31 in /docs (#446) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index 7c7a172c..42d7eaa5 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -6640,9 +6640,9 @@ postcss-zindex@^5.1.0: integrity sha512-fgFMf0OtVSBR1va1JNHYgMxYk73yhn/qb4uQDq1DLGYolz8gHCyr/sesEuGUaYs58E3ZJRcpoGuPVoB7Meiq9A== postcss@^8.3.11, postcss@^8.4.14, postcss@^8.4.17, postcss@^8.4.21: - version "8.4.27" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" - integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== dependencies: nanoid "^3.3.6" picocolors "^1.0.0" From e37f937abfcc05b0649f7fbf3dbc3045beba0bb0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Oct 2023 10:53:17 +0200 Subject: [PATCH 11/40] Bump postcss from 8.4.29 to 8.4.31 in /design (#447) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- design/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/design/package-lock.json b/design/package-lock.json index de4f50e7..bcfc5f8b 100644 --- a/design/package-lock.json +++ b/design/package-lock.json @@ -10017,9 +10017,9 @@ } }, "node_modules/postcss": { - "version": "8.4.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", - "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -19549,9 +19549,9 @@ } }, "postcss": { - "version": "8.4.29", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.29.tgz", - "integrity": "sha512-cbI+jaqIeu/VGqXEarWkRCCffhjgXc0qjBtXpqJhTBohMUjUQnbBr0xqX3vEKudc4iviTewcJo5ajcec5+wdJw==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "requires": { "nanoid": "^3.3.6", From eb1dd13ef7b22c12367408993d335e3694af5a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Wed, 4 Oct 2023 14:27:26 +0200 Subject: [PATCH 12/40] [FLORA-442] Implement package import from multiple repos (#444) --- .github/workflows/backend.yml | 2 +- .gitignore | 2 +- CHANGELOG.md | 1 + Makefile | 6 +- app/cli/Main.hs | 55 +++-- environment.test.sh | 2 + flora.cabal | 4 +- ...31002224826_add_url_to_package_indexes.sql | 2 + src/core/Flora/Environment/Config.hs | 2 +- src/core/Flora/Import/Package.hs | 185 ++++++++++++----- src/core/Flora/Import/Package/Bulk.hs | 108 +++++++--- src/core/Flora/Model/Category/Query.hs | 1 - src/core/Flora/Model/Job.hs | 1 - src/core/Flora/Model/Package/Query.hs | 3 +- src/core/Flora/Model/PackageIndex/Query.hs | 18 ++ .../Types.hs} | 32 +-- src/core/Flora/Model/PackageIndex/Update.hs | 32 +++ src/core/Flora/Model/Release/Query.hs | 38 ++-- src/jobs-worker/FloraJobs/Runner.hs | 24 --- src/jobs-worker/FloraJobs/Scheduler.hs | 19 +- src/web/FloraWeb/Pages/Routes/Admin.hs | 8 - src/web/FloraWeb/Pages/Server/Admin.hs | 16 +- src/web/FloraWeb/Pages/Server/Search.hs | 8 +- src/web/FloraWeb/Pages/Templates/Admin.hs | 13 +- .../FloraWeb/Pages/Templates/Pages/Search.hs | 19 +- test/Flora/ImportSpec.hs | 47 +++-- test/Flora/PackageSpec.hs | 16 +- test/Flora/TestUtils.hs | 7 +- test/Main.hs | 8 +- .../Cabal/{binary-0.2.cabal => binary.cabal} | 0 test/fixtures/Cabal/co-log.cabal | 189 ++++++++++++++++++ test/fixtures/Cabal/servant-server.cabal | 171 ++++++++++++++++ ...splay-0.0.4.0.cabal => text-display.cabal} | 0 .../Tarball/{ => tar-a/0.1.0.0}/tar-a.cabal | 4 +- .../Tarball/{ => tar-b/0.1.0.0}/tar-b.cabal | 0 test/fixtures/test-index.tar.gz | Bin 701 -> 734 bytes 36 files changed, 782 insertions(+), 261 deletions(-) create mode 100644 migrations/20231002224826_add_url_to_package_indexes.sql create mode 100644 src/core/Flora/Model/PackageIndex/Query.hs rename src/core/Flora/Model/{PackageIndex.hs => PackageIndex/Types.hs} (50%) create mode 100644 src/core/Flora/Model/PackageIndex/Update.hs rename test/fixtures/Cabal/{binary-0.2.cabal => binary.cabal} (100%) create mode 100644 test/fixtures/Cabal/co-log.cabal create mode 100644 test/fixtures/Cabal/servant-server.cabal rename test/fixtures/Cabal/{text-display-0.0.4.0.cabal => text-display.cabal} (100%) rename test/fixtures/Tarball/{ => tar-a/0.1.0.0}/tar-a.cabal (92%) rename test/fixtures/Tarball/{ => tar-b/0.1.0.0}/tar-b.cabal (100%) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 3f2a8db2..9759dfd3 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -101,6 +101,6 @@ jobs: migrate init "${FLORA_DB_CONNSTRING}" migrate migrate "${FLORA_DB_CONNSTRING}" migrations cabal run -- flora-cli create-user --username "hackage-user" --email "tech@flora.pm" --password "foobar2000" - cabal test + make test env: PGPASSWORD: "postgres" diff --git a/.gitignore b/.gitignore index f2702754..79f6e9c0 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ .hspec-failures .vscode/ /.stack-work/ -01-index +01-index* < Session.vim _data/ diff --git a/CHANGELOG.md b/CHANGELOG.md index bd4c7cd8..39b3e663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.0.14 -- XXXX-XX-XX * Added more matches to the the natural language processing catergory ([#44](/~https://github.com/flora-pm/flora-server/pull/440)) * Colourise in red deprecation markers on the package page ([#438](/~https://github.com/flora-pm/flora-server/pull/439)) +* Allow package imports from multiple repositories ([#444](/~https://github.com/flora-pm/flora-server/pull/444)) ## 1.0.13 -- 2023-09-17 * Exclude deprecated releases from latest versions and search ([#373](/~https://github.com/flora-pm/flora-server/pull/373)) diff --git a/Makefile b/Makefile index f42556d2..353d4ba0 100644 --- a/Makefile +++ b/Makefile @@ -42,11 +42,13 @@ db-migrate: ## Apply database migrations db-reset: db-drop db-setup db-provision ## Reset the dev database -db-provision: build ## Load the development data in the database +db-provision: ## Create categories and repositories @cabal run -- flora-cli create-user --username "hackage-user" --email "tech@flora.pm" --password "foobar2000" @cabal run -- flora-cli provision categories + @cabal run -- flora-cli provision-repository --name "hackage" --url https://hackage.haskell.org + @cabal run -- flora-cli provision-repository --name "cardano" --url https://input-output-hk.github.io/cardano-haskell-packages -db-provision-test-packages: +db-provision-test-packages: ## Load development data in the database @cabal run -- flora-cli provision test-packages import-from-hackage: ## Imports every cabal file from the ./index-01 directory diff --git a/app/cli/Main.hs b/app/cli/Main.hs index 01f93a51..0fdd3528 100644 --- a/app/cli/Main.hs +++ b/app/cli/Main.hs @@ -3,12 +3,12 @@ module Main where import Data.Maybe import Data.Password.Types import Data.Text (Text) +import Data.Text qualified as Text import DesignSystem (generateComponents) import Effectful import Effectful.Fail import Effectful.PostgreSQL.Transact.Effect import Effectful.Reader.Static (Reader, runReader) -import Flora.Model.User.Query qualified as Query import GHC.Generics (Generic) import Log.Backend.StandardOutput qualified as Log import Optics.Core @@ -18,7 +18,11 @@ import Flora.Environment import Flora.Environment.Config (PoolConfig (..)) import Flora.Import.Categories (importCategories) import Flora.Import.Package.Bulk (importAllFilesInRelativeDirectory, importFromIndex) +import Flora.Model.PackageIndex.Query qualified as Query +import Flora.Model.PackageIndex.Types +import Flora.Model.PackageIndex.Update qualified as Update import Flora.Model.User +import Flora.Model.User.Query qualified as Query import Flora.Model.User.Update data Options = Options @@ -30,8 +34,9 @@ data Command = Provision ProvisionTarget | CreateUser UserCreationOptions | GenDesignSystemComponents - | ImportPackages FilePath (Maybe Text) - | ImportIndex FilePath (Maybe Text) + | ImportPackages FilePath Text + | ImportIndex FilePath Text + | ProvisionRepository Text Text deriving stock (Show, Eq) data ProvisionTarget @@ -70,6 +75,7 @@ parseCommand = <> command "gen-design-system" (parseGenDesignSystem `withInfo` "Generate Design System components from the code") <> command "import-packages" (parseImportPackages `withInfo` "Import cabal packages from a directory") <> command "import-index" (parseImportIndex `withInfo` "Import cabal packages from the index tarball") + <> command "provision-repository" (parseProvisionRepository `withInfo` "Create a package repository") parseProvision :: Parser Command parseProvision = @@ -95,27 +101,23 @@ parseImportPackages :: Parser Command parseImportPackages = ImportPackages <$> argument str (metavar "PATH") - <*> optional - ( strOption $ - long "repository" - <> metavar "" - <> help "Which repository we're importing from" - ) + <*> option str (long "repository" <> metavar "" <> help "Which repository we're importing from (hackage, cardano…)") parseImportIndex :: Parser Command parseImportIndex = ImportIndex <$> argument str (metavar "PATH") - <*> optional - ( strOption $ - long "repository" - <> metavar "" - <> help "Which repository we're importing from" - ) + <*> option str (long "repository" <> metavar "" <> help "Which repository we're importing from (hackage, cardano…)") + +parseProvisionRepository :: Parser Command +parseProvisionRepository = + ProvisionRepository + <$> option str (long "name" <> metavar "" <> help "Name of the repository") + <*> option str (long "url" <> metavar "" <> help "Link to the package repository") runOptions :: (Reader PoolConfig :> es, DB :> es, Fail :> es, IOE :> es) => Options -> Eff es () runOptions (Options (Provision Categories)) = importCategories -runOptions (Options (Provision TestPackages)) = importFolderOfCabalFiles "./test/fixtures/Cabal/" Nothing +runOptions (Options (Provision TestPackages)) = importFolderOfCabalFiles "./test/fixtures/Cabal/" "hackage" runOptions (Options (CreateUser opts)) = do let username = opts ^. #username email = opts ^. #email @@ -135,16 +137,29 @@ runOptions (Options (CreateUser opts)) = do runOptions (Options GenDesignSystemComponents) = generateComponents runOptions (Options (ImportPackages path repository)) = importFolderOfCabalFiles path repository runOptions (Options (ImportIndex path repository)) = importIndex path repository +runOptions (Options (ProvisionRepository name url)) = provisionRepository name url + +provisionRepository :: (DB :> es, IOE :> es) => Text -> Text -> Eff es () +provisionRepository name url = do + Update.createPackageIndex name url Nothing -importFolderOfCabalFiles :: (Reader PoolConfig :> es, DB :> es, IOE :> es) => FilePath -> Maybe Text -> Eff es () +importFolderOfCabalFiles :: (Reader PoolConfig :> es, DB :> es, IOE :> es) => FilePath -> Text -> Eff es () importFolderOfCabalFiles path repository = Log.withStdOutLogger $ \appLogger -> do user <- fromJust <$> Query.getUserByUsername "hackage-user" - importAllFilesInRelativeDirectory appLogger (user ^. #userId) repository path True + mPackageIndex <- Query.getPackageIndexByName repository + case mPackageIndex of + Nothing -> error $ Text.unpack $ "Package index " <> repository <> " not found in the database!" + Just packageIndex -> + importAllFilesInRelativeDirectory appLogger (user ^. #userId) (repository, packageIndex.url) path True -importIndex :: (Reader PoolConfig :> es, DB :> es, IOE :> es) => FilePath -> Maybe Text -> Eff es () +importIndex :: (Reader PoolConfig :> es, DB :> es, IOE :> es) => FilePath -> Text -> Eff es () importIndex path repository = Log.withStdOutLogger $ \logger -> do user <- fromJust <$> Query.getUserByUsername "hackage-user" - importFromIndex logger (user ^. #userId) repository path True + mPackageIndex <- Query.getPackageIndexByName repository + case mPackageIndex of + Nothing -> error $ Text.unpack $ "Package index " <> repository <> " not found in the database!" + Just packageIndex -> + importFromIndex logger (user ^. #userId) (repository, packageIndex.url) path True withInfo :: Parser a -> String -> ParserInfo a withInfo opts desc = info (helper <*> opts) $ progDesc desc diff --git a/environment.test.sh b/environment.test.sh index 3fcec8c4..0ec5421d 100755 --- a/environment.test.sh +++ b/environment.test.sh @@ -9,3 +9,5 @@ export FLORA_DB_DATABASE="flora_test" export FLORA_DB_CONNSTRING="host=${FLORA_DB_HOST} dbname=${FLORA_DB_DATABASE} \ user=${FLORA_DB_USER} password=${FLORA_DB_PASSWORD} \ sslmode=allow" + +export FLORA_DB_POOL_CONNECTIONS=50 diff --git a/flora.cabal b/flora.cabal index 9ac1d7c9..c4565dd3 100644 --- a/flora.cabal +++ b/flora.cabal @@ -111,7 +111,9 @@ library Flora.Model.Package.Query Flora.Model.Package.Types Flora.Model.Package.Update - Flora.Model.PackageIndex + Flora.Model.PackageIndex.Query + Flora.Model.PackageIndex.Types + Flora.Model.PackageIndex.Update Flora.Model.PersistentSession Flora.Model.Release Flora.Model.Release.Query diff --git a/migrations/20231002224826_add_url_to_package_indexes.sql b/migrations/20231002224826_add_url_to_package_indexes.sql new file mode 100644 index 00000000..f0b25069 --- /dev/null +++ b/migrations/20231002224826_add_url_to_package_indexes.sql @@ -0,0 +1,2 @@ +alter table package_indexes + add url text not null; diff --git a/src/core/Flora/Environment/Config.hs b/src/core/Flora/Environment/Config.hs index c0f0580f..79928ab5 100644 --- a/src/core/Flora/Environment/Config.hs +++ b/src/core/Flora/Environment/Config.hs @@ -135,7 +135,7 @@ parsePoolConfig = <*> var (int >=> nonNegative) "FLORA_DB_POOL_CONNECTIONS" - (help "Number of connections per sub-pool") + (help "Number of connections across all sub-pools") parseLoggingEnv :: Parser Error LoggingEnv parseLoggingEnv = diff --git a/src/core/Flora/Import/Package.hs b/src/core/Flora/Import/Package.hs index a854f608..9d17ae1a 100644 --- a/src/core/Flora/Import/Package.hs +++ b/src/core/Flora/Import/Package.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE MultiWayIf #-} + -- | -- Module: Flora.Import.Package -- @@ -124,7 +126,21 @@ coreLibraries = versionList :: Set Version versionList = Set.fromList - [ Version.mkVersion [9, 4, 1] + [ Version.mkVersion [9, 8, 1] + , Version.mkVersion [9, 6, 3] + , Version.mkVersion [9, 6, 2] + , Version.mkVersion [9, 6, 1] + , Version.mkVersion [9, 4, 7] + , Version.mkVersion [9, 4, 6] + , Version.mkVersion [9, 4, 5] + , Version.mkVersion [9, 4, 4] + , Version.mkVersion [9, 4, 3] + , Version.mkVersion [9, 4, 2] + , Version.mkVersion [9, 4, 1] + , Version.mkVersion [9, 2, 8] + , Version.mkVersion [9, 2, 7] + , Version.mkVersion [9, 2, 6] + , Version.mkVersion [9, 2, 5] , Version.mkVersion [9, 2, 4] , Version.mkVersion [9, 2, 3] , Version.mkVersion [9, 2, 2] @@ -165,11 +181,13 @@ importFile => UserId -> FilePath -- ^ The absolute path to the Cabal file + -> (Text, Set PackageName) + -- ^ The name of the repository -> Eff es () -importFile userId path = +importFile userId path repo = withWorkerDbPool $ \wq -> loadFile path - >>= uncurry (extractPackageDataFromCabal userId Nothing) + >>= uncurry (extractPackageDataFromCabal userId repo) >>= persistImportOutput wq enqueueImportJob :: (DB :> es, IOE :> es) => ImportOutput -> Eff es () @@ -186,10 +204,15 @@ enqueueImportJob importOutput = do (ImportPackage importOutput) ) -importRelFile :: (Time :> es, Reader PoolConfig :> es, DB :> es, IOE :> es, Log :> es) => UserId -> FilePath -> Eff es () -importRelFile user dir = do +importRelFile + :: (Time :> es, Reader PoolConfig :> es, DB :> es, IOE :> es, Log :> es) + => UserId + -> FilePath + -> (Text, Set PackageName) + -> Eff es () +importRelFile user dir repo = do workdir <- ( dir) <$> liftIO System.getCurrentDirectory - importFile user workdir + importFile user workdir repo -- | Loads and parses a Cabal file loadFile @@ -227,10 +250,15 @@ parseString parser name bs = do Log.logAttention_ (display $ show err) throw $ CabalFileCouldNotBeParsed name -loadAndExtractCabalFile :: (IOE :> es, Log :> es, Time :> es) => UserId -> FilePath -> Eff es ImportOutput -loadAndExtractCabalFile userId filePath = +loadAndExtractCabalFile + :: (IOE :> es, Log :> es, Time :> es) + => UserId + -> FilePath + -> (Text, Set PackageName) + -> Eff es ImportOutput +loadAndExtractCabalFile userId filePath repo = loadFile filePath - >>= uncurry (extractPackageDataFromCabal userId Nothing) + >>= uncurry (extractPackageDataFromCabal userId repo) -- | Persists an 'ImportOutput' to the database. An 'ImportOutput' can be obtained -- by extracting relevant information from a Cabal file using 'extractPackageDataFromCabal' @@ -269,21 +297,30 @@ withWorkerDbPool :: (Reader PoolConfig :> es, IOE :> es) => (Poolboy.WorkQueue - withWorkerDbPool f = do cfg <- ask @PoolConfig withEffToIO $ \effIO -> - Poolboy.withPoolboy (Poolboy.poolboySettingsWith cfg.connections) Poolboy.waitingStopFinishWorkers $ \wq -> - effIO $ f wq + Poolboy.withPoolboy + (Poolboy.poolboySettingsWith cfg.connections) + Poolboy.waitingStopFinishWorkers + $ \wq -> + effIO $ f wq -- | Transforms a 'GenericPackageDescription' from Cabal into an 'ImportOutput' -- that can later be inserted into the database. This function produces stable, deterministic ids, -- so it should be possible to extract and insert a single package many times in a row. -extractPackageDataFromCabal :: (IOE :> es, Time :> es) => UserId -> Maybe Text -> UTCTime -> GenericPackageDescription -> Eff es ImportOutput -extractPackageDataFromCabal userId repository uploadTime genericDesc = do +extractPackageDataFromCabal + :: (IOE :> es, Time :> es) + => UserId + -> (Text, Set PackageName) + -> UTCTime + -> GenericPackageDescription + -> Eff es ImportOutput +extractPackageDataFromCabal userId (repositoryName, repositoryPackages) uploadTime genericDesc = do let packageDesc = genericDesc.packageDescription let flags = Vector.fromList genericDesc.genPackageFlags let packageName = force $ packageDesc ^. #package % #pkgName % to unPackageName % to pack % to PackageName let packageVersion = force packageDesc.package.pkgVersion - let namespace = force $ chooseNamespace packageName - let packageId = force $ deterministicPackageId namespace packageName - let releaseId = force $ deterministicReleaseId packageId packageVersion + let namespace = chooseNamespace packageName repositoryName repositoryPackages + let packageId = deterministicPackageId namespace packageName + let releaseId = deterministicReleaseId packageId packageVersion timestamp <- Time.currentTime let sourceRepos = getRepoURL packageName packageDesc.sourceRepos let rawCategoryField = packageDesc ^. #category % to Cabal.fromShortText % to T.pack @@ -314,7 +351,7 @@ extractPackageDataFromCabal userId repository uploadTime genericDesc = do , readmeStatus = NotImported , changelog = Nothing , changelogStatus = NotImported - , repository + , repository = Just repositoryName , license = Cabal.license packageDesc , sourceRepos , homepage = Just $ display packageDesc.homepage @@ -329,21 +366,21 @@ extractPackageDataFromCabal userId repository uploadTime genericDesc = do , revisedAt = Nothing } - let lib = extractLibrary package release Nothing [] <$> allLibraries packageDesc - let condLib = maybe [] (extractCondTree extractLibrary package release Nothing) genericDesc.condLibrary - let condSubLibs = extractCondTrees extractLibrary package release genericDesc.condSubLibraries + let lib = extractLibrary package (repositoryName, repositoryPackages) release Nothing [] <$> allLibraries packageDesc + let condLib = maybe [] (extractCondTree extractLibrary package (repositoryName, repositoryPackages) release Nothing) genericDesc.condLibrary + let condSubLibs = extractCondTrees extractLibrary package (repositoryName, repositoryPackages) release genericDesc.condSubLibraries - let foreignLibs = extractForeignLib package release Nothing [] <$> packageDesc.foreignLibs - let condForeignLibs = extractCondTrees extractForeignLib package release genericDesc.condForeignLibs + let foreignLibs = extractForeignLib package (repositoryName, repositoryPackages) release Nothing [] <$> packageDesc.foreignLibs + let condForeignLibs = extractCondTrees extractForeignLib package (repositoryName, repositoryPackages) release genericDesc.condForeignLibs - let executables = extractExecutable package release Nothing [] <$> packageDesc.executables - let condExecutables = extractCondTrees extractExecutable package release genericDesc.condExecutables + let executables = extractExecutable package (repositoryName, repositoryPackages) release Nothing [] <$> packageDesc.executables + let condExecutables = extractCondTrees extractExecutable package (repositoryName, repositoryPackages) release genericDesc.condExecutables - let testSuites = extractTestSuite package release Nothing [] <$> packageDesc.testSuites - let condTestSuites = extractCondTrees extractTestSuite package release genericDesc.condTestSuites + let testSuites = extractTestSuite package (repositoryName, repositoryPackages) release Nothing [] <$> packageDesc.testSuites + let condTestSuites = extractCondTrees extractTestSuite package (repositoryName, repositoryPackages) release genericDesc.condTestSuites - let benchmarks = extractBenchmark package release Nothing [] <$> packageDesc.benchmarks - let condBenchmarks = extractCondTrees extractBenchmark package release genericDesc.condBenchmarks + let benchmarks = extractBenchmark package (repositoryName, repositoryPackages) release Nothing [] <$> packageDesc.benchmarks + let condBenchmarks = extractCondTrees extractBenchmark package (repositoryName, repositoryPackages) release genericDesc.condBenchmarks let components = lib @@ -359,40 +396,76 @@ extractPackageDataFromCabal userId repository uploadTime genericDesc = do <> condBenchmarks pure ImportOutput{..} -extractLibrary :: Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> Library -> ImportComponent -extractLibrary package = +extractLibrary + :: Package + -> (Text, Set PackageName) + -> Release + -> Maybe UnqualComponentName + -> [Condition ConfVar] + -> Library + -> ImportComponent +extractLibrary package repository = genericComponentExtractor Component.Library (^. #libName % to getLibName) (^. #libBuildInfo % #targetBuildDepends) package + repository where getLibName :: LibraryName -> Text getLibName LMainLibName = display package.name getLibName (LSubLibName lname) = T.pack $ unUnqualComponentName lname -extractForeignLib :: Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> ForeignLib -> ImportComponent +extractForeignLib + :: Package + -> (Text, Set PackageName) + -> Release + -> Maybe UnqualComponentName + -> [Condition ConfVar] + -> ForeignLib + -> ImportComponent extractForeignLib = genericComponentExtractor Component.ForeignLib (^. #foreignLibName % to unUnqualComponentName % to T.pack) (^. #foreignLibBuildInfo % #targetBuildDepends) -extractExecutable :: Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> Executable -> ImportComponent +extractExecutable + :: Package + -> (Text, Set PackageName) + -> Release + -> Maybe UnqualComponentName + -> [Condition ConfVar] + -> Executable + -> ImportComponent extractExecutable = genericComponentExtractor Component.Executable (^. #exeName % to unUnqualComponentName % to T.pack) (^. #buildInfo % #targetBuildDepends) -extractTestSuite :: Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> TestSuite -> ImportComponent +extractTestSuite + :: Package + -> (Text, Set PackageName) + -> Release + -> Maybe UnqualComponentName + -> [Condition ConfVar] + -> TestSuite + -> ImportComponent extractTestSuite = genericComponentExtractor Component.TestSuite (^. #testName % to unUnqualComponentName % to T.pack) (^. #testBuildInfo % #targetBuildDepends) -extractBenchmark :: Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> Benchmark -> ImportComponent +extractBenchmark + :: Package + -> (Text, Set PackageName) + -> Release + -> Maybe UnqualComponentName + -> [Condition ConfVar] + -> Benchmark + -> ImportComponent extractBenchmark = genericComponentExtractor Component.Benchmark @@ -402,16 +475,17 @@ extractBenchmark = -- | Traverses the provided 'CondTree' and applies the given 'ComponentExtractor' -- to every node, returning a list of 'ImportComponent' extractCondTree - :: (Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> component -> ImportComponent) + :: (Package -> (Text, Set PackageName) -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> component -> ImportComponent) -> Package + -> (Text, Set PackageName) -> Release -> Maybe UnqualComponentName -> CondTree ConfVar [Dependency] component -> [ImportComponent] -extractCondTree extractor package release defaultComponentName = go [] +extractCondTree extractor package repository release defaultComponentName = go [] where go cond tree = - let treeComponent = extractor package release defaultComponentName cond tree.condTreeData + let treeComponent = extractor package repository release defaultComponentName cond tree.condTreeData treeSubComponents = tree.condTreeComponents >>= extractBranch in treeComponent : treeSubComponents extractBranch CondBranch{condBranchCondition, condBranchIfTrue, condBranchIfFalse} = @@ -423,13 +497,14 @@ extractCondTree extractor package release defaultComponentName = go [] -- This function builds upon 'extractCondTree' to make it easier to extract fields such as 'condExecutables', 'condTestSuites' etc. -- from a 'GenericPackageDescription' extractCondTrees - :: (Package -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> component -> ImportComponent) + :: (Package -> (Text, Set PackageName) -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] -> component -> ImportComponent) -> Package + -> (Text, Set PackageName) -> Release -> [(UnqualComponentName, CondTree ConfVar [Dependency] component)] -> [ImportComponent] -extractCondTrees extractor package release trees = - trees >>= \case (name, tree) -> extractCondTree extractor package release (Just name) tree +extractCondTrees extractor package repository release trees = + trees >>= \case (name, tree) -> extractCondTree extractor package repository release (Just name) tree genericComponentExtractor :: forall component @@ -440,6 +515,7 @@ genericComponentExtractor -> (component -> [Dependency]) -- ^ Extract dependencies -> Package + -> (Text, Set PackageName) -> Release -> Maybe UnqualComponentName -> [Condition ConfVar] @@ -450,6 +526,7 @@ genericComponentExtractor getName getDeps package + repository release defaultComponentName condition @@ -460,13 +537,18 @@ genericComponentExtractor componentId = deterministicComponentId releaseId canonicalForm metadata = ComponentMetadata (ComponentCondition <$> condition) component = PackageComponent{..} - dependencies = force $ buildDependency package componentId <$> getDeps rawComponent - in force (component, dependencies) - -buildDependency :: Package -> ComponentId -> Cabal.Dependency -> ImportDependency -buildDependency package packageComponentId (Cabal.Dependency depName versionRange _) = + dependencies = buildDependency package repository componentId <$> getDeps rawComponent + in (component, dependencies) + +buildDependency + :: Package + -> (Text, Set PackageName) + -> ComponentId + -> Cabal.Dependency + -> ImportDependency +buildDependency package (repository, repositoryPackages) packageComponentId (Cabal.Dependency depName versionRange _) = let name = depName & unPackageName & pack & PackageName - namespace = chooseNamespace name + namespace = chooseNamespace name repository repositoryPackages packageId = deterministicPackageId namespace name ownerId = package.ownerId createdAt = package.createdAt @@ -482,15 +564,18 @@ buildDependency package packageComponentId (Cabal.Dependency depName versionRang , requirement = display . prettyShow $ versionRange , metadata = RequirementMetadata{flag = Nothing} } - in force $ ImportDependency{package = dependencyPackage, requirement} + in ImportDependency{package = dependencyPackage, requirement} getRepoURL :: PackageName -> [Cabal.SourceRepo] -> Vector Text getRepoURL _ [] = Vector.empty getRepoURL _ (repo : _) = Vector.singleton $ display $ fromMaybe mempty repo.repoLocation -chooseNamespace :: PackageName -> Namespace -chooseNamespace name | Set.member name coreLibraries = Namespace "haskell" -chooseNamespace _ = Namespace "hackage" +chooseNamespace :: PackageName -> Text -> Set PackageName -> Namespace +chooseNamespace name repo repositoryPackages = + if + | name `Set.member` coreLibraries -> Namespace "haskell" + | name `Set.member` repositoryPackages -> Namespace repo + | otherwise -> Namespace "hackage" extractTestedWith :: Vector (CompilerFlavor, VersionRange) -> Vector VersionRange extractTestedWith testedWithVector = diff --git a/src/core/Flora/Import/Package/Bulk.hs b/src/core/Flora/Import/Package/Bulk.hs index 95f51ea5..7c3483e1 100644 --- a/src/core/Flora/Import/Package/Bulk.hs +++ b/src/core/Flora/Import/Package/Bulk.hs @@ -1,17 +1,25 @@ {-# LANGUAGE AllowAmbiguousTypes #-} {-# OPTIONS_GHC -fno-full-laziness #-} -module Flora.Import.Package.Bulk (importAllFilesInDirectory, importAllFilesInRelativeDirectory, importFromIndex) where +module Flora.Import.Package.Bulk + ( importAllFilesInDirectory + , importAllFilesInRelativeDirectory + , importFromIndex + ) where +import Codec.Archive.Tar (Entries) import Codec.Archive.Tar qualified as Tar import Codec.Archive.Tar.Entry qualified as Tar +import Codec.Archive.Tar.Index qualified as Tar import Codec.Compression.GZip qualified as GZip -import Control.Monad (join, when, (>=>)) +import Control.Monad (when, (>=>)) import Data.ByteString qualified as BS import Data.ByteString.Lazy qualified as BL import Data.Function ((&)) import Data.List (isSuffixOf) -import Data.Maybe (fromMaybe, isNothing) +import Data.Maybe (fromMaybe) +import Data.Set (Set) +import Data.Set qualified as Set import Data.Text (Text) import Data.Text qualified as Text import Data.Time (UTCTime) @@ -24,14 +32,24 @@ import Effectful.Time (runTime) import Log (Logger, defaultLogLevel) import Streamly.Data.Fold qualified as SFold import Streamly.Prelude qualified as S +import System.Directory import System.Directory qualified as System import System.FilePath import UnliftIO.Exception (finally) import Flora.Environment.Config (PoolConfig (..)) -import Flora.Import.Package (enqueueImportJob, extractPackageDataFromCabal, loadContent, persistImportOutput, withWorkerDbPool) +import Flora.Import.Package + ( enqueueImportJob + , extractPackageDataFromCabal + , loadContent + , persistImportOutput + , withWorkerDbPool + ) +import Flora.Model.Package import Flora.Model.Package.Update qualified as Update -import Flora.Model.PackageIndex (getPackageIndexTimestamp, updatePackageIndexTimestamp) +import Flora.Model.PackageIndex.Query qualified as Query +import Flora.Model.PackageIndex.Types +import Flora.Model.PackageIndex.Update qualified as Update import Flora.Model.Release.Query qualified as Query import Flora.Model.Release.Update qualified as Update import Flora.Model.User @@ -41,27 +59,41 @@ importAllFilesInRelativeDirectory :: (Reader PoolConfig :> es, DB :> es, IOE :> es) => Logger -> UserId - -> Maybe Text + -> (Text, Text) -> FilePath -> Bool -> Eff es () -importAllFilesInRelativeDirectory appLogger user repository dir directImport = do +importAllFilesInRelativeDirectory appLogger user (repositoryName, repositoryURL) dir directImport = do workdir <- ( dir) <$> liftIO System.getCurrentDirectory - importAllFilesInDirectory appLogger user repository workdir directImport + importAllFilesInDirectory appLogger user (repositoryName, repositoryURL) workdir directImport importFromIndex :: (Reader PoolConfig :> es, DB :> es, IOE :> es) => Logger -> UserId - -> Maybe Text + -> (Text, Text) -> FilePath -> Bool -> Eff es () -importFromIndex appLogger user repository index directImport = do +importFromIndex appLogger user (repositoryName, repositoryURL) index directImport = do entries <- Tar.read . GZip.decompress <$> liftIO (BL.readFile index) - time <- fromMaybe (posixSecondsToUTCTime 0) . join <$> traverse getPackageIndexTimestamp repository + let Right repositoryPackages = buildPackageListFromArchive entries + mPackageIndex <- Query.getPackageIndexByName repositoryName + time <- case mPackageIndex of + Nothing -> pure $ posixSecondsToUTCTime 0 + Just packageIndex -> + pure $ + fromMaybe + (posixSecondsToUTCTime 0) + packageIndex.timestamp case Tar.foldlEntries (buildContentStream time) S.nil entries of - Right stream -> importFromStream appLogger user repository directImport stream + Right stream -> + importFromStream + appLogger + user + (repositoryName, repositoryURL, repositoryPackages) + directImport + stream Left (err, _) -> Log.runLog "flora-cli" appLogger defaultLogLevel $ Log.logAttention_ $ @@ -81,28 +113,27 @@ importAllFilesInDirectory :: (Reader PoolConfig :> es, DB :> es, IOE :> es) => Logger -> UserId - -> Maybe Text + -> (Text, Text) -> FilePath -> Bool -> Eff es () -importAllFilesInDirectory appLogger user repository dir directImport = do +importAllFilesInDirectory appLogger user (repositoryName, repositoryURL) dir directImport = do liftIO $ System.createDirectoryIfMissing True dir + packages <- buildPackageListFromDirectory dir liftIO . putStrLn $ "🔎 Searching cabal files in " <> dir - importFromStream appLogger user repository directImport $ findAllCabalFilesInDirectory dir + importFromStream appLogger user (repositoryName, repositoryURL, packages) directImport $ findAllCabalFilesInDirectory dir importFromStream :: (Reader PoolConfig :> es, DB :> es, IOE :> es) => Logger -> UserId - -> Maybe Text + -> (Text, Text, Set PackageName) -> Bool -> S.AsyncT IO (String, UTCTime, BS.ByteString) -> Eff es () -importFromStream appLogger user repository directImport stream = do +importFromStream appLogger user (repositoryName, repositoryURL, repositoryPackages) directImport stream = do pool <- getPool poolConfig <- ask @PoolConfig - -- create a packageindex if it doesn't exist - maybe (pure ()) createPkgIdx repository processedPackageCount <- finally ( withWorkerDbPool $ \wq -> @@ -113,19 +144,14 @@ importFromStream appLogger user repository directImport stream = do ) -- We want to refresh db and update latest timestamp even if we fell -- over at some point - ( Update.refreshLatestVersions - >> Update.refreshDependents - >> maybe (pure ()) updatePkgIdxTimestamp repository + ( do + Update.refreshLatestVersions + Update.refreshDependents + timestamp <- Query.getLatestReleaseTime (Just repositoryName) + Update.updatePackageIndexByName repositoryName timestamp ) displayStats processedPackageCount where - updatePkgIdxTimestamp repository' = - Query.getLatestReleaseTime (Just repository') - >>= updatePackageIndexTimestamp repository' - createPkgIdx repo = do - pkgIndexTz <- getPackageIndexTimestamp repo - when (isNothing pkgIndexTz) $ - updatePackageIndexTimestamp repo Nothing displayCount = flip SFold.foldlM' (return 0) $ \previousCount _ -> @@ -142,7 +168,7 @@ importFromStream appLogger user repository directImport stream = do . Log.runLog "flora-cli" appLogger defaultLogLevel . ( \(path, timestamp, content) -> loadContent path content - >>= ( extractPackageDataFromCabal user repository timestamp + >>= ( extractPackageDataFromCabal user (repositoryName, repositoryPackages) timestamp >=> \importedPackage -> if directImport then persistImportOutput wq importedPackage @@ -167,3 +193,25 @@ findAllCabalFilesInDirectory workdir = S.concatMapM traversePath $ S.fromList [w timestamp <- System.getModificationTime p return $ S.fromPure (p, timestamp, content) _ -> return S.nil + +buildPackageListFromArchive :: Entries e -> Either e (Set PackageName) +buildPackageListFromArchive entries = + case Tar.build entries of + Left e -> Left e + Right tarIndex -> + Tar.toList tarIndex + & fmap (takeDirectory . takeDirectory . fst) + & filter (/= ".") + & fmap (PackageName . Text.pack) + & Set.fromList + & Right + +buildPackageListFromDirectory :: IOE :> es => FilePath -> Eff es (Set PackageName) +buildPackageListFromDirectory dir = do + paths <- liftIO $ listDirectory dir + paths + & fmap takeBaseName + & filter (/= ".") + & fmap (PackageName . Text.pack) + & Set.fromList + & pure diff --git a/src/core/Flora/Model/Category/Query.hs b/src/core/Flora/Model/Category/Query.hs index 054fc179..ddd1e1cd 100644 --- a/src/core/Flora/Model/Category/Query.hs +++ b/src/core/Flora/Model/Category/Query.hs @@ -35,7 +35,6 @@ getPackagesFromCategorySlug slug = liftIO $ T.putStrLn $ "Could not find category from slug: \"" <> slug <> "\"" pure Vector.empty Just Category{categoryId} -> do - liftIO $ T.putStrLn "Category found!" dbtToEff $ joinSelectOneByField @Package @PackageCategory [field| package_id |] diff --git a/src/core/Flora/Model/Job.hs b/src/core/Flora/Model/Job.hs index 2774c6dd..0636f9a5 100644 --- a/src/core/Flora/Model/Job.hs +++ b/src/core/Flora/Model/Job.hs @@ -73,7 +73,6 @@ data FloraOddJobs = FetchReadme ReadmeJobPayload | FetchUploadTime UploadTimeJobPayload | FetchChangelog ChangelogJobPayload - | ImportHackageIndex ImportHackageIndexPayload | ImportPackage ImportOutput | FetchPackageDeprecationList | FetchReleaseDeprecationList PackageName (Vector ReleaseId) diff --git a/src/core/Flora/Model/Package/Query.hs b/src/core/Flora/Model/Package/Query.hs index 9e63e29d..f09bdd46 100644 --- a/src/core/Flora/Model/Package/Query.hs +++ b/src/core/Flora/Model/Package/Query.hs @@ -251,7 +251,8 @@ getAllRequirementsQuery = getRequirementsQuery :: Query getRequirementsQuery = [sql| - select distinct dependency.namespace, dependency.name, req.requirement from requirements as req + select distinct dependency.namespace, dependency.name, req.requirement + from requirements as req inner join packages as dependency on dependency.package_id = req.package_id inner join package_components as pc ON pc.package_component_id = req.package_component_id and (pc.component_type = 'library') diff --git a/src/core/Flora/Model/PackageIndex/Query.hs b/src/core/Flora/Model/PackageIndex/Query.hs new file mode 100644 index 00000000..74a12585 --- /dev/null +++ b/src/core/Flora/Model/PackageIndex/Query.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE QuasiQuotes #-} + +module Flora.Model.PackageIndex.Query where + +import Data.Text (Text) +import Database.PostgreSQL.Entity (selectOneByField) +import Database.PostgreSQL.Entity.Types +import Database.PostgreSQL.Simple (Only (..)) +import Effectful +import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff) + +import Flora.Model.PackageIndex.Types + +getPackageIndexByName :: DB :> es => Text -> Eff es (Maybe PackageIndex) +getPackageIndexByName repository = + dbtToEff $ + selectOneByField [field| repository |] (Only repository) diff --git a/src/core/Flora/Model/PackageIndex.hs b/src/core/Flora/Model/PackageIndex/Types.hs similarity index 50% rename from src/core/Flora/Model/PackageIndex.hs rename to src/core/Flora/Model/PackageIndex/Types.hs index ae3f4d84..20f0f6c8 100644 --- a/src/core/Flora/Model/PackageIndex.hs +++ b/src/core/Flora/Model/PackageIndex/Types.hs @@ -1,26 +1,20 @@ -{-# LANGUAGE OverloadedLists #-} -{-# LANGUAGE QuasiQuotes #-} - -module Flora.Model.PackageIndex where +module Flora.Model.PackageIndex.Types where import GHC.Generics import Control.DeepSeq (NFData) -import Control.Monad (void) import Data.Text (Text) import Data.Text.Display import Data.Time (UTCTime) import Data.UUID import Data.UUID.V4 qualified as UUID -import Database.PostgreSQL.Entity (insert, selectOneByField, update) import Database.PostgreSQL.Entity.Types -import Database.PostgreSQL.Simple (Only (..)) import Database.PostgreSQL.Simple.FromField (FromField (..)) import Database.PostgreSQL.Simple.FromRow (FromRow (..)) import Database.PostgreSQL.Simple.ToField (ToField (..)) import Database.PostgreSQL.Simple.ToRow (ToRow (..)) import Effectful -import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff) +import Text.Regex.Pcre2 newtype PackageIndexId = PackageIndexId {getPackageIndexId :: UUID} deriving stock (Generic) @@ -31,6 +25,7 @@ newtype PackageIndexId = PackageIndexId {getPackageIndexId :: UUID} data PackageIndex = PackageIndex { packageIndexId :: PackageIndexId , repository :: Text + , url :: Text , timestamp :: Maybe UTCTime } deriving stock (Eq, Show, Generic) @@ -39,22 +34,11 @@ data PackageIndex = PackageIndex (Entity) via (GenericEntity '[TableName "package_indexes"] PackageIndex) -mkPackageIndex :: IOE :> es => Text -> Maybe UTCTime -> Eff es PackageIndex -mkPackageIndex repository timestamp = do +mkPackageIndex :: IOE :> es => Text -> Text -> Maybe UTCTime -> Eff es PackageIndex +mkPackageIndex repository url timestamp = do packageIndexId <- PackageIndexId <$> liftIO UUID.nextRandom pure $ PackageIndex{..} -getPackageIndexTimestamp :: DB :> es => Text -> Eff es (Maybe UTCTime) -getPackageIndexTimestamp repository = do - res :: Maybe PackageIndex <- dbtToEff $ selectOneByField [field| repository |] (Only repository) - pure $ res >>= timestamp - -updatePackageIndexTimestamp :: (IOE :> es, DB :> es) => Text -> Maybe UTCTime -> Eff es () -updatePackageIndexTimestamp repository timestamp = do - packageIndex <- mkPackageIndex repository timestamp - void $ - dbtToEff $ - selectOneByField @PackageIndex [field| repository |] (Only repository) - >>= maybe - (insert @PackageIndex packageIndex) - (\pkgIx -> update @PackageIndex pkgIx{timestamp}) +parseRepository :: Text -> Bool +parseRepository txt = + matches "[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*" txt diff --git a/src/core/Flora/Model/PackageIndex/Update.hs b/src/core/Flora/Model/PackageIndex/Update.hs new file mode 100644 index 00000000..4d63389d --- /dev/null +++ b/src/core/Flora/Model/PackageIndex/Update.hs @@ -0,0 +1,32 @@ +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE QuasiQuotes #-} + +module Flora.Model.PackageIndex.Update + ( updatePackageIndexByName + , createPackageIndex + ) where + +import Control.Monad (void) +import Data.Text (Text) +import Data.Time (UTCTime) +import Database.PostgreSQL.Entity (insert, updateFieldsBy) +import Database.PostgreSQL.Entity.Types +import Database.PostgreSQL.Simple (Only (..)) +import Effectful +import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff) + +import Flora.Model.PackageIndex.Types + +updatePackageIndexByName :: DB :> es => Text -> Maybe UTCTime -> Eff es () +updatePackageIndexByName repositoryName newTimestamp = do + void $ + dbtToEff $ + updateFieldsBy @PackageIndex + [[field| timestamp |]] + ([field| repository |], repositoryName) + (Only newTimestamp) + +createPackageIndex :: (IOE :> es, DB :> es) => Text -> Text -> Maybe UTCTime -> Eff es () +createPackageIndex repositoryName url timestamp = do + packageIndex <- mkPackageIndex repositoryName url timestamp + void $ dbtToEff $ insert @PackageIndex packageIndex diff --git a/src/core/Flora/Model/Release/Query.hs b/src/core/Flora/Model/Release/Query.hs index 101336c0..2f0a8453 100644 --- a/src/core/Flora/Model/Release/Query.hs +++ b/src/core/Flora/Model/Release/Query.hs @@ -5,14 +5,14 @@ module Flora.Model.Release.Query ( getReleases , getRelease , getReleaseByVersion - , getPackageReleasesWithoutReadme - , getPackageReleasesWithoutChangelog - , getPackageReleasesWithoutUploadTimestamp + , getHackagePackageReleasesWithoutReadme + , getHackagePackageReleasesWithoutChangelog + , getHackagePackageReleasesWithoutUploadTimestamp , getAllReleases , getLatestReleaseTime , getNumberOfReleases , getReleaseComponents - , getPackagesWithoutReleaseDeprecationInformation + , getHackagePackagesWithoutReleaseDeprecationInformation , getVersionFromManyReleaseIds ) where @@ -76,10 +76,10 @@ getVersionFromManyReleaseIds releaseIds = do where r0.release_id in ? |] -getPackageReleasesWithoutReadme +getHackagePackageReleasesWithoutReadme :: DB :> es => Eff es (Vector (ReleaseId, Version, PackageName)) -getPackageReleasesWithoutReadme = +getHackagePackageReleasesWithoutReadme = dbtToEff $ query Select querySpec () where @@ -91,29 +91,33 @@ getPackageReleasesWithoutReadme = join packages as p on p.package_id = r.package_id where r.readme_status = 'not-imported' + and p.namespace = 'hackage' + or p.namespace = 'haskell' |] -getPackageReleasesWithoutUploadTimestamp +getHackagePackageReleasesWithoutUploadTimestamp :: DB :> es => Eff es (Vector (ReleaseId, Version, PackageName)) -getPackageReleasesWithoutUploadTimestamp = +getHackagePackageReleasesWithoutUploadTimestamp = dbtToEff $ query Select querySpec () where querySpec :: Query querySpec = [sql| - select r.release_id, r.version, p."name" + select r."release_id", r."version", p."name" from releases as r join packages as p - on p.package_id = r.package_id - where r.uploaded_at is null + on p."package_id" = r."package_id" + where r."uploaded_at" is null + and p."namespace" = 'hackage' + or p."namespace" = 'haskell' |] -getPackageReleasesWithoutChangelog +getHackagePackageReleasesWithoutChangelog :: DB :> es => Eff es (Vector (ReleaseId, Version, PackageName)) -getPackageReleasesWithoutChangelog = +getHackagePackageReleasesWithoutChangelog = dbtToEff $ query Select querySpec () where @@ -125,12 +129,14 @@ getPackageReleasesWithoutChangelog = join packages as p on p.package_id = r.package_id where r.changelog_status = 'not-imported' + and p.namespace = 'hackage' + or p.namespace = 'haskell' |] -getPackagesWithoutReleaseDeprecationInformation +getHackagePackagesWithoutReleaseDeprecationInformation :: DB :> es => Eff es (Vector (PackageName, Vector ReleaseId)) -getPackagesWithoutReleaseDeprecationInformation = +getHackagePackagesWithoutReleaseDeprecationInformation = dbtToEff $ query_ Select q where q = @@ -139,6 +145,8 @@ getPackagesWithoutReleaseDeprecationInformation = from releases as r0 join packages as p1 on r0.package_id = p1.package_id where r0.deprecated is null + and p1.namespace = 'hackage' + or p1.namespace = 'haskell' group by p1.name; |] diff --git a/src/jobs-worker/FloraJobs/Runner.hs b/src/jobs-worker/FloraJobs/Runner.hs index 6fab1422..68ae9bc5 100644 --- a/src/jobs-worker/FloraJobs/Runner.hs +++ b/src/jobs-worker/FloraJobs/Runner.hs @@ -1,6 +1,5 @@ module FloraJobs.Runner where -import Control.Concurrent (forkIO) import Control.Exception import Control.Monad import Control.Monad.IO.Class @@ -10,13 +9,11 @@ import Data.Set qualified as Set import Data.Text.Display import Data.Vector (Vector) import Data.Vector qualified as Vector -import Effectful.PostgreSQL.Transact.Effect import Log import Network.HTTP.Types (gone410, notFound404, statusCode) import OddJobs.Job (Job (..)) import Servant.Client (ClientError (..)) import Servant.Client.Core (ResponseF (..)) -import System.Process.Typed qualified as System import Flora.Import.Package (coreLibraries, persistImportOutput, withWorkerDbPool) import Flora.Model.Job @@ -26,30 +23,10 @@ import Flora.Model.Release.Query qualified as Query import Flora.Model.Release.Types import Flora.Model.Release.Update qualified as Update import FloraJobs.Render (renderMarkdown) -import FloraJobs.Scheduler import FloraJobs.ThirdParties.Hackage.API (HackagePackageInfo (..), HackagePreferredVersions (..), VersionedPackage (..)) import FloraJobs.ThirdParties.Hackage.Client qualified as Hackage import FloraJobs.Types -fetchNewIndex :: JobsRunner () -fetchNewIndex = - localDomain "index-import" $ do - logInfo_ "Fetching new index" - System.runProcess_ "cabal update" - System.runProcess_ "cp ~/.cabal/packages/hackage.haskell.org/01-index.tar 01-index/" - System.runProcess_ "cd 01-index && tar -xf 01-index.tar" - System.runProcess_ "make import-from-hackage" - logInfo_ "New index processed" - releases <- Query.getPackageReleasesWithoutReadme - pool <- getPool - liftIO $ - forkIO $ - forM_ - releases - ( \(releaseId, version, packagename) -> scheduleReadmeJob pool releaseId packagename version - ) - liftIO $ void $ scheduleIndexImportJob pool - runner :: Job -> JobsRunner () runner job = localDomain "job-runner" $ case fromJSON (jobPayload job) of @@ -58,7 +35,6 @@ runner job = localDomain "job-runner" $ FetchReadme x -> makeReadme x FetchUploadTime x -> fetchUploadTime x FetchChangelog x -> fetchChangeLog x - ImportHackageIndex _ -> fetchNewIndex ImportPackage x -> withWorkerDbPool $ \wq -> persistImportOutput wq x diff --git a/src/jobs-worker/FloraJobs/Scheduler.hs b/src/jobs-worker/FloraJobs/Scheduler.hs index d5d8fa5a..bbd1f056 100644 --- a/src/jobs-worker/FloraJobs/Scheduler.hs +++ b/src/jobs-worker/FloraJobs/Scheduler.hs @@ -5,7 +5,6 @@ module FloraJobs.Scheduler ( scheduleReadmeJob , scheduleChangelogJob , scheduleUploadTimeJob - , scheduleIndexImportJob , schedulePackageDeprecationListJob , scheduleReleaseDeprecationListJob , scheduleRefreshLatestVersions @@ -19,7 +18,6 @@ module FloraJobs.Scheduler where import Data.Pool -import Data.Time qualified as Time import Data.Vector (Vector) import Database.PostgreSQL.Entity.DBT import Database.PostgreSQL.Simple (Only (..)) @@ -27,9 +25,8 @@ import Database.PostgreSQL.Simple qualified as PG import Database.PostgreSQL.Simple.SqlQQ (sql) import Distribution.Types.Version import Effectful.PostgreSQL.Transact.Effect -import Effectful.Time qualified as Time import Log -import OddJobs.Job (Job (..), createJob, scheduleJob) +import OddJobs.Job (Job (..), createJob) import Flora.Model.Job import Flora.Model.Package @@ -69,20 +66,6 @@ scheduleUploadTimeJob pool releaseId packageName version = (FetchUploadTime $ UploadTimeJobPayload packageName releaseId (MkIntAesonVersion version)) ) -scheduleIndexImportJob :: Pool PG.Connection -> IO Job -scheduleIndexImportJob pool = - withResource - pool - ( \conn -> do - t <- Time.currentTime - let runAt = Time.addUTCTime Time.nominalDay t - scheduleJob - conn - jobTableName - (ImportHackageIndex ImportHackageIndexPayload) - runAt - ) - schedulePackageDeprecationListJob :: Pool PG.Connection -> IO Job schedulePackageDeprecationListJob pool = withResource diff --git a/src/web/FloraWeb/Pages/Routes/Admin.hs b/src/web/FloraWeb/Pages/Routes/Admin.hs index 13110494..63cb3179 100644 --- a/src/web/FloraWeb/Pages/Routes/Admin.hs +++ b/src/web/FloraWeb/Pages/Routes/Admin.hs @@ -17,17 +17,9 @@ type FetchMetadata = type FetchMetadataResponse = Headers '[Header "Location" Text] NoContent -type ImportIndex = - "index-import" - :> Verb 'POST 301 '[HTML] ImportIndexResponse - -type ImportIndexResponse = - Headers '[Header "Location" Text] NoContent - data Routes' mode = Routes' { index :: mode :- Get '[HTML] (Html ()) , fetchMetadata :: mode :- FetchMetadata - , importIndex :: mode :- ImportIndex , oddJobs :: mode :- "odd-jobs" :> OddJobs.FinalAPI -- they compose :o , users :: mode :- "users" :> AdminUsersRoutes , packages :: mode :- "packages" :> PackagesAdminRoutes diff --git a/src/web/FloraWeb/Pages/Server/Admin.hs b/src/web/FloraWeb/Pages/Server/Admin.hs index d5a260ae..70051d5c 100644 --- a/src/web/FloraWeb/Pages/Server/Admin.hs +++ b/src/web/FloraWeb/Pages/Server/Admin.hs @@ -40,7 +40,6 @@ server cfg env = , packages = adminPackagesHandler , oddJobs = OddJobs.server cfg env handlerToEff , fetchMetadata = fetchMetadataHandler - , importIndex = indexImportJobHandler } -- | This function converts a sub-tree of routes that require 'Admin' role @@ -75,7 +74,7 @@ fetchMetadataHandler = do liftIO $ void $ schedulePackageDeprecationListJob jobsPool - releasesWithoutReadme <- Query.getPackageReleasesWithoutReadme + releasesWithoutReadme <- Query.getHackagePackageReleasesWithoutReadme liftIO $ void $ forkIO $ @@ -84,7 +83,7 @@ fetchMetadataHandler = do ( \(releaseId, version, packagename) -> scheduleReadmeJob jobsPool releaseId packagename version ) - releasesWithoutUploadTime <- Query.getPackageReleasesWithoutUploadTimestamp + releasesWithoutUploadTime <- Query.getHackagePackageReleasesWithoutUploadTimestamp liftIO $ void $ forkIO $ @@ -93,7 +92,7 @@ fetchMetadataHandler = do ( \(releaseId, version, packagename) -> scheduleUploadTimeJob jobsPool releaseId packagename version ) - releasesWithoutChangelog <- Query.getPackageReleasesWithoutChangelog + releasesWithoutChangelog <- Query.getHackagePackageReleasesWithoutChangelog liftIO $ void $ forkIO $ @@ -102,7 +101,7 @@ fetchMetadataHandler = do ( \(releaseId, version, packagename) -> scheduleChangelogJob jobsPool releaseId packagename version ) - packagesWithoutDeprecationInformation <- Query.getPackagesWithoutReleaseDeprecationInformation + packagesWithoutDeprecationInformation <- Query.getHackagePackagesWithoutReleaseDeprecationInformation liftIO $ void $ forkIO $ do @@ -114,13 +113,6 @@ fetchMetadataHandler = do pure $ redirect "/admin" -indexImportJobHandler :: FloraAdmin ImportIndexResponse -indexImportJobHandler = do - session <- getSession - FloraEnv{jobsPool} <- liftIO $ fetchFloraEnv session.webEnvStore - liftIO $ void $ scheduleIndexImportJob jobsPool - pure $ redirect "/admin" - adminUsersHandler :: ServerT AdminUsersRoutes FloraAdmin adminUsersHandler = AdminUsersRoutes' diff --git a/src/web/FloraWeb/Pages/Server/Search.hs b/src/web/FloraWeb/Pages/Server/Search.hs index 76e2b003..18612442 100644 --- a/src/web/FloraWeb/Pages/Server/Search.hs +++ b/src/web/FloraWeb/Pages/Server/Search.hs @@ -36,9 +36,5 @@ searchHandler (Just searchString) pageParam = do session <- getSession templateEnv <- fromSession session defaultTemplateEnv (count, results) <- Search.searchPackageByName (fromPage pageNumber) searchString - let (matchVector, rest) = Vector.partition (\p -> p.name == PackageName searchString) results - let (mExactMatch, packagesInfo) = - case Vector.uncons matchVector of - Just (exactResult, _) -> (Just exactResult, rest) - Nothing -> (Nothing, rest) - render templateEnv $ Search.showResults searchString count pageNumber mExactMatch packagesInfo + let (matchVector, packagesInfo) = Vector.partition (\p -> p.name == PackageName searchString) results + render templateEnv $ Search.showResults searchString count pageNumber matchVector packagesInfo diff --git a/src/web/FloraWeb/Pages/Templates/Admin.hs b/src/web/FloraWeb/Pages/Templates/Admin.hs index 49422395..4a0357ec 100644 --- a/src/web/FloraWeb/Pages/Templates/Admin.hs +++ b/src/web/FloraWeb/Pages/Templates/Admin.hs @@ -34,20 +34,11 @@ dataReport adminReport = do div_ [class_ "admin-card"] $ do dt_ [class_ ""] - "README, CHANGELOG, Upload time…" + "README, CHANGELOG, Upload time, Revision time, deprecation information" dd_ [class_ ""] $ form_ [action_ "/admin/metadata", method_ "POST"] $ do - button_ [class_ ""] "Fetch release metadata" - - div_ [class_ "admin-card"] $ do - dt_ - [class_ ""] - "Index import" - - dd_ [class_ ""] $ - form_ [action_ "/admin/index-import", method_ "POST"] $ do - button_ [class_ ""] "Schedule" + button_ [class_ ""] "Fetch Hackage releases metadata" a_ [href_ "/admin/odd-jobs"] $ div_ [class_ "admin-card"] $ do diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Search.hs b/src/web/FloraWeb/Pages/Templates/Pages/Search.hs index 0be62482..c079e2ed 100644 --- a/src/web/FloraWeb/Pages/Templates/Pages/Search.hs +++ b/src/web/FloraWeb/Pages/Templates/Pages/Search.hs @@ -1,13 +1,13 @@ module FloraWeb.Pages.Templates.Pages.Search where import Control.Monad (when) +import Data.Foldable (forM_) +import Data.Positive import Data.Text (Text) import Data.Text.Display (display) import Data.Vector (Vector) import Lucid -import Data.Maybe (fromJust, isJust) -import Data.Positive import Flora.Model.Package (Namespace, PackageInfo (..)) import Flora.Search (SearchAction (..)) import FloraWeb.Components.PackageListHeader (presentationHeader) @@ -30,12 +30,19 @@ showAllPackagesInNamespace namespace count currentPage packagesInfo = do div_ [class_ ""] $ packageListing packagesInfo paginationNav count currentPage (ListAllPackagesInNamespace namespace) -showResults :: Text -> Word -> Positive Word -> Maybe PackageInfo -> Vector PackageInfo -> FloraHTML -showResults searchString count currentPage mExactMatch results = do +showResults + :: Text + -> Word + -> Positive Word + -> Vector PackageInfo + -- ^ Exact matches + -> Vector PackageInfo + -- ^ Results + -> FloraHTML +showResults searchString count currentPage exactMatches results = do div_ [class_ "container"] $ do presentationHeader searchString "" count - when (isJust mExactMatch) $ do - let em = fromJust mExactMatch + forM_ exactMatches $ \em -> do div_ [class_ "exact-match"] $ packageListItem (em.namespace, em.name, em.synopsis, em.version, em.license) div_ [class_ ""] $ packageListing results diff --git a/test/Flora/ImportSpec.hs b/test/Flora/ImportSpec.hs index 728d8942..cc31e671 100644 --- a/test/Flora/ImportSpec.hs +++ b/test/Flora/ImportSpec.hs @@ -2,14 +2,17 @@ module Flora.ImportSpec where import Data.Foldable (traverse_) import Data.Maybe (catMaybes) -import Data.Time.Format.ISO8601 +import Data.Set qualified as Set +import Data.Text (Text) import Log.Backend.StandardOutput (withStdOutLogger) import Optics.Core +import Flora.Import.Package (chooseNamespace) import Flora.Import.Package.Bulk import Flora.Model.Package.Query qualified as Query import Flora.Model.Package.Types -import Flora.Model.PackageIndex +import Flora.Model.PackageIndex.Query qualified as Query +import Flora.Model.PackageIndex.Update qualified as Update import Flora.Model.Release.Query qualified as Query import Flora.Model.Release.Types import Flora.Model.User @@ -18,28 +21,42 @@ import Flora.TestUtils spec :: Fixtures -> TestEff TestTree spec fixtures = testThese - "import tests" + "Import tests" [ testThis "Import index" $ testImportIndex fixtures + , testThis "Namespace choosser" testNamespaceChooser ] +testIndex :: FilePath +testIndex = "./test/fixtures/test-index.tar.gz" + +defaultRepo :: Text +defaultRepo = "test-namespace" + +defaultRepoURL :: Text +defaultRepoURL = "localhost" + testImportIndex :: Fixtures -> TestEff () testImportIndex fixture = withStdOutLogger $ \logger -> do - let testIndex = "./test/fixtures/test-index.tar.gz" - defaultRepo = "hackage.haskell.org" + mIndex <- Query.getPackageIndexByName defaultRepo + case mIndex of + Nothing -> Update.createPackageIndex defaultRepo defaultRepoURL Nothing + Just _ -> pure () importFromIndex logger - (fixture ^. #hackageUser % #userId) - (Just defaultRepo) + (fixture.hackageUser.userId) + (defaultRepo, defaultRepoURL) testIndex True - -- Check the expected timestamp - timestamp <- getPackageIndexTimestamp defaultRepo - expectedTimestamp <- iso8601ParseM "2010-01-01T00:00:00Z" - assertEqual (Just expectedTimestamp) timestamp -- check the packages have been imported - tars <- traverse (Query.getPackageByNamespaceAndName (Namespace "hackage") . PackageName) ["tar-a", "tar-b"] + tars <- traverse (Query.getPackageByNamespaceAndName (Namespace defaultRepo) . PackageName) ["tar-a", "tar-b"] releases <- fmap mconcat . traverse (\x -> Query.getReleases (x ^. #packageId)) $ catMaybes tars - assertEqual (length tars) 2 - assertEqual (length releases) 2 - traverse_ (\x -> assertEqual (x ^. #repository) (Just "hackage.haskell.org")) releases + assertEqual 2 (length tars) + assertEqual 2 (length releases) + traverse_ (\x -> assertEqual (x ^. #repository) (Just defaultRepo)) releases + +testNamespaceChooser :: TestEff () +testNamespaceChooser = do + assertEqual + (chooseNamespace (PackageName "tar-a") defaultRepo (Set.fromList [PackageName "tar-a", PackageName "tar-b"])) + (Namespace defaultRepo) diff --git a/test/Flora/PackageSpec.hs b/test/Flora/PackageSpec.hs index 50c76b20..6e63458f 100644 --- a/test/Flora/PackageSpec.hs +++ b/test/Flora/PackageSpec.hs @@ -52,7 +52,7 @@ spec _fixtures = testCabalDeps :: TestEff () testCabalDeps = do dependencies <- do - Just cabalPackage <- Query.getPackageByNamespaceAndName (Namespace "haskell") (PackageName "Cabal") + cabalPackage <- assertJust =<< Query.getPackageByNamespaceAndName (Namespace "haskell") (PackageName "Cabal") releases <- Query.getReleases (cabalPackage ^. #packageId) let latestRelease = maximumBy (compare `on` (.version)) releases Query.getAllRequirements (latestRelease ^. #releaseId) @@ -137,7 +137,7 @@ testBytestringDependents :: TestEff () testBytestringDependents = do results <- Query.getAllPackageDependentsWithLatestVersion (Namespace "haskell") (PackageName "bytestring") (0, 30) assertEqual - 22 + 24 (Vector.length results) testNoSelfDependent :: TestEff () @@ -147,6 +147,7 @@ testNoSelfDependent = do assertEqual ( Set.fromList [ PackageName "Cabal" + , PackageName "co-log" , PackageName "flora" , PackageName "hashable" , PackageName "jose" @@ -155,6 +156,7 @@ testNoSelfDependent = do , PackageName "relude" , PackageName "saturn" , PackageName "semigroups" + , PackageName "servant-server" , PackageName "text-display" , PackageName "xml" ] @@ -209,7 +211,7 @@ testPackagesDeprecation = do [ DeprecatedPackage (PackageName "integer-gmp") alternative1 , DeprecatedPackage (PackageName "mtl") alternative2 ] - integerGmp <- fromJust <$> Query.getPackageByNamespaceAndName (Namespace "haskell") (PackageName "integer-gmp") + integerGmp <- assertJust =<< Query.getPackageByNamespaceAndName (Namespace "haskell") (PackageName "integer-gmp") assertEqual (Just alternative1) integerGmp.deprecationInfo testGetNonDeprecatedPackages :: TestEff () @@ -222,13 +224,13 @@ testGetNonDeprecatedPackages = do testReleaseDeprecation :: TestEff () testReleaseDeprecation = do - result <- Query.getPackagesWithoutReleaseDeprecationInformation - assertEqual 64 (length result) + result <- Query.getHackagePackagesWithoutReleaseDeprecationInformation + assertEqual 66 (length result) binary <- fromJust <$> Query.getPackageByNamespaceAndName (Namespace "haskell") (PackageName "binary") - Just deprecatedBinaryVersion' <- Query.getReleaseByVersion (binary.packageId) (mkVersion [0, 10, 0, 0]) + deprecatedBinaryVersion' <- assertJust =<< Query.getReleaseByVersion (binary.packageId) (mkVersion [0, 10, 0, 0]) Update.setReleasesDeprecationMarker (Vector.singleton (True, deprecatedBinaryVersion'.releaseId)) - Just deprecatedBinaryVersion <- Query.getReleaseByVersion (binary.packageId) (mkVersion [0, 10, 0, 0]) + deprecatedBinaryVersion <- assertJust =<< Query.getReleaseByVersion (binary.packageId) (mkVersion [0, 10, 0, 0]) assertEqual deprecatedBinaryVersion.deprecated (Just True) --- diff --git a/test/Flora/TestUtils.hs b/test/Flora/TestUtils.hs index d864ad3e..d75630b8 100644 --- a/test/Flora/TestUtils.hs +++ b/test/Flora/TestUtils.hs @@ -11,6 +11,7 @@ module Flora.TestUtils , assertBool , assertEqual , assertFailure + , assertJust , assertRight , assertRight' , assertClientRight @@ -123,7 +124,7 @@ importAllPackages fixtures = Log.withStdOutLogger $ \appLogger -> do importAllFilesInRelativeDirectory appLogger (fixtures ^. #hackageUser % #userId) - Nothing + ("hackage", "https://hackage.haskell.org") "./test/fixtures/Cabal/" True @@ -165,6 +166,10 @@ assertEqual expected actual = liftIO $ Test.assertEqual "" expected actual assertFailure :: MonadIO m => String -> m () assertFailure = liftIO . Test.assertFailure +assertJust :: HasCallStack => Maybe a -> TestEff a +assertJust (Just a) = pure a +assertJust Nothing = liftIO $ Test.assertFailure "Test return Nothing instead of Just" + assertRight :: HasCallStack => Either a b -> TestEff b assertRight (Left _a) = liftIO $ Test.assertFailure "Test return Left instead of Right" assertRight (Right b) = pure b diff --git a/test/Main.hs b/test/Main.hs index 173bf599..ce37df39 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -6,15 +6,16 @@ import Effectful.Log qualified as Log import Effectful.PostgreSQL.Transact.Effect import Effectful.Reader.Static (runReader) import Effectful.Time -import Flora.CabalSpec qualified as CabalSpec -import Flora.CategorySpec qualified as CategorySpec -import Flora.Environment import Log.Backend.StandardOutput qualified as Log import Log.Data import System.IO import Test.Tasty (defaultMain, testGroup) +import Flora.CabalSpec qualified as CabalSpec +import Flora.CategorySpec qualified as CategorySpec +import Flora.Environment import Flora.ImportSpec qualified as ImportSpec +import Flora.Model.PackageIndex.Update qualified as Update import Flora.OddJobSpec qualified as OddJobSpec import Flora.PackageSpec qualified as PackageSpec import Flora.TemplateSpec qualified as TemplateSpec @@ -32,6 +33,7 @@ main = do . runReader env.dbConfig . runFailIO $ do + Update.createPackageIndex "hackage" "" Nothing testMigrations f' <- getFixtures importAllPackages f' diff --git a/test/fixtures/Cabal/binary-0.2.cabal b/test/fixtures/Cabal/binary.cabal similarity index 100% rename from test/fixtures/Cabal/binary-0.2.cabal rename to test/fixtures/Cabal/binary.cabal diff --git a/test/fixtures/Cabal/co-log.cabal b/test/fixtures/Cabal/co-log.cabal new file mode 100644 index 00000000..79c49ac8 --- /dev/null +++ b/test/fixtures/Cabal/co-log.cabal @@ -0,0 +1,189 @@ +cabal-version: 2.4 +name: co-log +version: 0.6.0.2 +synopsis: Composable Contravariant Comonadic Logging Library +description: + The default implementation of logging based on [co-log-core](http://hackage.haskell.org/package/co-log-core). + . + The ideas behind this package are described in the following blog post: + . + * [co-log: Composable Contravariant Combinatorial Comonadic Configurable Convenient Logging](https://kowainik.github.io/posts/2018-09-25-co-log) + +homepage: /~https://github.com/co-log/co-log +bug-reports: /~https://github.com/co-log/co-log/issues +license: MPL-2.0 +license-file: LICENSE +author: Dmitrii Kovanikov +maintainer: Kowainik +copyright: 2018-2022 Kowainik, 2023 Co-Log +category: Logging, Contravariant, Comonad +build-type: Simple +stability: provisional +extra-doc-files: CHANGELOG.md + README.md +tested-with: GHC == 8.10.7 + GHC == 9.0.2 + GHC == 9.2.8 + GHC == 9.4.7 + GHC == 9.6.2 + +flag tutorial + description: Controls if tutorials get build (mainly to avoid building them on hackage). + default: False + +source-repository head + type: git + location: /~https://github.com/co-log/co-log.git + +common common-options + build-depends: base >= 4.14 && < 4.19 + + ghc-options: -Wall + -Wcompat + -Widentities + -Wincomplete-uni-patterns + -Wincomplete-record-updates + -Wredundant-constraints + if impl(ghc >= 8.2) + ghc-options: -fhide-source-paths + if impl(ghc >= 8.4) + ghc-options: -Wmissing-export-lists + -Wpartial-fields + if impl(ghc >= 8.8) + ghc-options: -Wmissing-deriving-strategies + if impl(ghc >= 8.10) + ghc-options: -Wunused-packages + if impl(ghc >= 9.0) + ghc-options: -Winvalid-haddock + if impl(ghc >= 9.2) + ghc-options: -Wredundant-bang-patterns + -Woperator-whitespace + + default-language: Haskell2010 + default-extensions: ConstraintKinds + DerivingStrategies + DeriveGeneric + GeneralizedNewtypeDeriving + LambdaCase + OverloadedStrings + RecordWildCards + ScopedTypeVariables + StandaloneDeriving + TupleSections + TypeApplications + ViewPatterns + +common tutorial-options + import: common-options + if os(windows) || !flag(tutorial) + buildable: False + build-depends: co-log-core + , text + + build-tool-depends: markdown-unlit:markdown-unlit >= 0.5.0 && < 0.7 + ghc-options: -pgmL markdown-unlit + +common tutorial-depends + import: tutorial-options + build-depends: co-log + , mtl + + +library + import: common-options + hs-source-dirs: src + exposed-modules: Colog + Colog.Actions + Colog.Contra + Colog.Message + Colog.Monad + Colog.Pure + Colog.Rotation + + build-depends: ansi-terminal >= 1.0 && < 1.1 + , bytestring >= 0.10.8 && < 0.13 + , co-log-core ^>= 0.3 + , containers >= 0.5.7 && < 0.7 + , contravariant ^>= 1.5 + , directory ^>= 1.3.0 + , exceptions >= 0.8.3 && < 0.11 + , filepath ^>= 1.4.1 + , mtl >= 2.2.2 && < 2.4 + , text >= 1.2.3 && < 2.2 + , chronos ^>= 1.1 && < 1.2 + , transformers >= 0.5 && < 0.7 + , dependent-sum >= 0.7 && < 0.8 + , dependent-map >= 0.4 && < 0.5 + , unliftio-core ^>= 0.2 + , vector >= 0.12.0.3 && < 0.14 + if impl(ghc < 9.4.5) + build-depends: run-st <= 0.1.3.0 + +executable play-colog + import: common-options + hs-source-dirs: tutorials + main-is: Main.hs + + build-depends: co-log + , mtl + , dependent-map + + ghc-options: -threaded + -rtsopts + -with-rtsopts=-N + +executable concurrent-playground + import: common-options + hs-source-dirs: tutorials + main-is: Concurrent.hs + build-depends: bytestring + , co-log + ghc-options: -threaded + -rtsopts + -with-rtsopts=-N + +executable readme + import: tutorial-options + main-is: README.lhs + build-depends: co-log + , text + +test-suite test-co-log + import: common-options + build-depends: co-log + , co-log-core + , hedgehog >= 1.0 && < 1.5 + hs-source-dirs: test + main-is: Property.hs + type: exitcode-stdio-1.0 + +test-suite co-log-doctest + import: common-options + -- Disable `doctest` on windows since it couldn't handle qualified imports reliable (which leads to errors like "Not in scope: `C.timeToOffsetDatetime'"). + if os(windows) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: test + main-is: Doctest.hs + + build-depends: doctest >= 0.16.0 && < 0.23 + , Glob ^>= 0.10.0 + ghc-options: -threaded + + +executable tutorial-intro + import: tutorial-options + main-is: tutorials/1-intro/Intro.lhs + +executable tutorial-loggert-simple + import: tutorial-depends + main-is: tutorials/2-loggert/loggert.lhs + +executable tutorial-loggert + import: tutorial-depends + main-is: tutorials/3-loggert-with-message/loggert.lhs + +executable tutorial-custom + import: tutorial-depends + main-is: tutorials/custom/Custom.lhs + diff --git a/test/fixtures/Cabal/servant-server.cabal b/test/fixtures/Cabal/servant-server.cabal new file mode 100644 index 00000000..16a642ce --- /dev/null +++ b/test/fixtures/Cabal/servant-server.cabal @@ -0,0 +1,171 @@ +cabal-version: 2.2 +name: servant-server +version: 0.20 +x-revision: 2 + +synopsis: A family of combinators for defining webservices APIs and serving them +category: Servant, Web +description: + A family of combinators for defining webservices APIs and serving them + . + You can learn about the basics in the . + . + + is a runnable example, with comments, that defines a dummy API and implements + a webserver that serves this API, using this package. + . + + +homepage: http://docs.servant.dev/ +bug-reports: http://github.com/haskell-servant/servant/issues +license: BSD-3-Clause +license-file: LICENSE +author: Servant Contributors +maintainer: haskell-servant-maintainers@googlegroups.com +copyright: 2014-2016 Zalora South East Asia Pte Ltd, 2016-2019 Servant Contributors +build-type: Simple +tested-with: GHC==8.6.5, GHC==8.8.4, GHC ==8.10.7, GHC ==9.0.2, GHC ==9.2.7, GHC ==9.4.4 + +extra-source-files: + CHANGELOG.md + README.md + +source-repository head + type: git + location: http://github.com/haskell-servant/servant.git + +library + exposed-modules: + Servant + Servant.Server + Servant.Server.Experimental.Auth + Servant.Server.Generic + Servant.Server.Internal + Servant.Server.Internal.BasicAuth + Servant.Server.Internal.Context + Servant.Server.Internal.Delayed + Servant.Server.Internal.DelayedIO + Servant.Server.Internal.ErrorFormatter + Servant.Server.Internal.Handler + Servant.Server.Internal.RouteResult + Servant.Server.Internal.Router + Servant.Server.Internal.RoutingApplication + Servant.Server.Internal.ServerError + Servant.Server.StaticFiles + Servant.Server.UVerb + + -- deprecated + exposed-modules: + Servant.Utils.StaticFiles + + -- Bundled with GHC: Lower bound to not force re-installs + -- text and mtl are bundled starting with GHC-8.4 + build-depends: + base >= 4.9 && < 4.19 + , bytestring >= 0.10.8.1 && < 0.12 + , constraints >= 0.2 && < 0.14 + , containers >= 0.5.7.1 && < 0.7 + , mtl ^>= 2.2.2 || ^>= 2.3.1 + , text >= 1.2.3.0 && < 2.1 + , transformers >= 0.5.2.0 && < 0.7 + , filepath >= 1.4.1.1 && < 1.5 + + -- Servant dependencies + -- strict dependency as we re-export 'servant' things. + build-depends: + servant >= 0.20 && < 0.21 + , http-api-data >= 0.4.1 && < 0.7 + + -- Other dependencies: Lower bound around what is in the latest Stackage LTS. + -- Here can be exceptions if we really need features from the newer versions. + build-depends: + base-compat >= 0.10.5 && < 0.14 + , base64-bytestring >= 1.0.0.1 && < 1.3 + , exceptions >= 0.10.0 && < 0.11 + , http-media >= 0.7.1.3 && < 0.9 + , http-types >= 0.12.2 && < 0.13 + , network-uri >= 2.6.1.0 && < 2.8 + , monad-control >= 1.0.2.3 && < 1.1 + , network >= 2.8 && < 3.2 + , sop-core >= 0.4.0.0 && < 0.6 + , string-conversions >= 0.4.0.1 && < 0.5 + , resourcet >= 1.2.2 && < 1.4 + , tagged >= 0.8.6 && < 0.9 + , transformers-base >= 0.4.5.2 && < 0.5 + , wai >= 3.2.2.1 && < 3.3 + , wai-app-static >= 3.1.6.2 && < 3.2 + , word8 >= 0.1.3 && < 0.2 + + hs-source-dirs: src + default-language: Haskell2010 + + ghc-options: -Wall -Wno-redundant-constraints + +executable greet + main-is: greet.hs + hs-source-dirs: example + ghc-options: -Wall + default-language: Haskell2010 + build-depends: + base + , base-compat + , servant + , servant-server + , wai + , text + + build-depends: + aeson >= 1.4.1.0 && < 3 + , warp >= 3.2.25 && < 3.4 + +test-suite spec + type: exitcode-stdio-1.0 + ghc-options: -Wall + default-language: Haskell2010 + hs-source-dirs: test + main-is: Spec.hs + other-modules: + Servant.ArbitraryMonadServerSpec + Servant.Server.ErrorSpec + Servant.Server.Internal.ContextSpec + Servant.Server.Internal.RoutingApplicationSpec + Servant.Server.RouterSpec + Servant.Server.StaticFilesSpec + Servant.Server.StreamingSpec + Servant.Server.UsingContextSpec + Servant.Server.UsingContextSpec.TestCombinators + Servant.HoistSpec + Servant.ServerSpec + + -- Dependencies inherited from the library. No need to specify bounds. + build-depends: + base + , base-compat + , base64-bytestring + , bytestring + , http-types + , mtl + , resourcet + , safe + , servant + , servant-server + , sop-core + , string-conversions + , text + , transformers + , transformers-compat + , wai + + -- Additional dependencies + build-depends: + aeson >= 1.4.1.0 && < 3 + , directory >= 1.3.0.0 && < 1.4 + , hspec >= 2.6.0 && < 2.12 + , hspec-wai >= 0.10.1 && < 0.12 + , QuickCheck >= 2.12.6.1 && < 2.15 + , should-not-typecheck >= 2.1.0 && < 2.2 + , temporary >= 1.3 && < 1.4 + , wai-extra >= 3.0.24.3 && < 3.2 + + build-tool-depends: + hspec-discover:hspec-discover >= 2.6.0 && <2.12 diff --git a/test/fixtures/Cabal/text-display-0.0.4.0.cabal b/test/fixtures/Cabal/text-display.cabal similarity index 100% rename from test/fixtures/Cabal/text-display-0.0.4.0.cabal rename to test/fixtures/Cabal/text-display.cabal diff --git a/test/fixtures/Tarball/tar-a.cabal b/test/fixtures/Tarball/tar-a/0.1.0.0/tar-a.cabal similarity index 92% rename from test/fixtures/Tarball/tar-a.cabal rename to test/fixtures/Tarball/tar-a/0.1.0.0/tar-a.cabal index 3b141c4e..d1eb0333 100644 --- a/test/fixtures/Tarball/tar-a.cabal +++ b/test/fixtures/Tarball/tar-a/0.1.0.0/tar-a.cabal @@ -18,7 +18,7 @@ executable e Main-is: A.hs default-language: Haskell2010 build-depends: base >=4 && <5 - , b:{ sublib + , tar-b:{ sublib , anothersublib -- You can include sublibraries from hackage, like --, cabal-plan:{topograph} @@ -29,7 +29,7 @@ library -- other-modules: -- other-extensions: build-depends: base >=4 && <5 - , b:{ sublib + , tar-b:{ sublib , anothersublib } -- hs-source-dirs: default-language: Haskell2010 diff --git a/test/fixtures/Tarball/tar-b.cabal b/test/fixtures/Tarball/tar-b/0.1.0.0/tar-b.cabal similarity index 100% rename from test/fixtures/Tarball/tar-b.cabal rename to test/fixtures/Tarball/tar-b/0.1.0.0/tar-b.cabal diff --git a/test/fixtures/test-index.tar.gz b/test/fixtures/test-index.tar.gz index 606a8fe03c0dcf6791e284191d87222fc36067de..d65c69a7b4d8ea227551fec88eb5385fb1b80b04 100644 GIT binary patch literal 734 zcmV<40wMh$iwFP!000001MQYgZ<|06hJEH&>?tRM7aM|v+Dd7qw3XUZdg*b2F{}bC z+K(tsl>gpgu~Wy2lQxQptL}5Lc0J6_>^sJg~&|PwC_2l+Ps8uUE#Z}rb3Q)P^U0;J; z*F1+-LFzH1DaWWEwc=HM@R4GLpERje)Hn<#^(@Gt~&qghr10gsUHc(jk* zkh7ptxGrGHOs)MIpE0Z)&iFE1=bVNVLxuth=%6jsriP3uARpdG^i;Gq7$WLU3Is=z&pLFD;9d~3Pk zOu%|8pvD#zGgv{fg7JuxO8{^t6)dbW4JxQ)X_wdtmG-4kM8e_UV3>&5|QHVgQ zIvhfTLu38g@(M%GYuOSvX>1u*)7wdMBnsjn8q?LQR(6`}yf-S;rQ%(m!(aX^8y!%+C>{g1`jTs`B)R9`MRqhp*i^?~v z?qLPzeAx-~@Y*QmYiU)9{(=kChX@RV>@S29FfbIK!qK)mEPn~Fpco>!yHad;C<@t_I6+jMScs~7J?W1 zJw`9q%6`UAIZ{V{N=sd7X*o(^D_qA_OmWn(P;vpDUIb4AB!^`cg*jo;{4Xg zPS%Qb&E4Y(-g(}5(TaTVx|13!e<>?FwEHynhV+IKw#N_8Xh^gqv@*l-ak~k3V zY)Qq{kjEbf|1bal-}S$EHT{q2o1e{+biUUABzF3L4E{#{i?^Wv#cS&S0agDA!TNL| zyCX?fueMvkdY%exD!Gqy#WgRu;AWLW*B_R9t30>GA*y^{6`LY Date: Wed, 4 Oct 2023 14:45:34 +0200 Subject: [PATCH 13/40] Add social media badges --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 26f92068..a0b096f0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@

- CI badge + CI badge made with Haskell @@ -18,6 +18,16 @@ Matrix chatroom badge + + + Mastodon + + + + + Static Badge + +

From 8b4eab534ad652211728656ce259333ca063e145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sun, 8 Oct 2023 13:52:49 +0200 Subject: [PATCH 14/40] [FLORA-450] Add a guide for namespaces (#451) --- CHANGELOG.md | 1 + docs/docs/intro.md | 4 ++++ docs/docs/namespaces.md | 44 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 docs/docs/namespaces.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 39b3e663..7fddb66e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Added more matches to the the natural language processing catergory ([#44](/~https://github.com/flora-pm/flora-server/pull/440)) * Colourise in red deprecation markers on the package page ([#438](/~https://github.com/flora-pm/flora-server/pull/439)) * Allow package imports from multiple repositories ([#444](/~https://github.com/flora-pm/flora-server/pull/444)) +* Add a page on namespaces in the documentation ([#451](/~https://github.com/flora-pm/flora-server/pull/451)) ## 1.0.13 -- 2023-09-17 * Exclude deprecated releases from latest versions and search ([#373](/~https://github.com/flora-pm/flora-server/pull/373)) diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 35d24642..5bac245c 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -2,3 +2,7 @@ title: Flora.pm slug: / --- + +Read more about: + +* [Namespaces](/namespaces) diff --git a/docs/docs/namespaces.md b/docs/docs/namespaces.md new file mode 100644 index 00000000..ab581bc7 --- /dev/null +++ b/docs/docs/namespaces.md @@ -0,0 +1,44 @@ +--- +title: Flora Namespaces +slug: namespaces +--- + +In Flora, packages are categorised in a namespace to mark their provenance. They start with a `@` and allow us to refer to packages unambiguously or mark their importance. + +## @haskell Packages + +Some packages are foundational to the ecosystem and maintained by either the Core Libraries Committee or the GHC team, and this makes them unique in terms of the expectations we have from them. + +These packages live in the [`@haskell`] namespace to show that they are stable and reliable. Some example of packages are `base`, `text`, `bytestring`, and `mtl`. + +## @hackage Packages + +This is where most third-party packages live, which you will find on [Hackage](https://hackage.haskell.org). + +## @cardano Packages + +Flora also indexes the [Cardano Haskell Packages (CHaP)][CHaP], an index of packages by the Cardano project. +These packages live under the [`@cardano`] namespace. + +To use them in your own project, insert the following configuration in your `cabal.project` file: + +``` +repository cardano + url: https://input-output-hk.github.io/cardano-haskell-packages + secure: True + root-keys: + 3e0cce471cf09815f930210f7827266fd09045445d65923e6d0238a6cd15126f + 443abb7fb497a134c343faf52f0b659bd7999bc06b7f63fa76dc99d631f9bea1 + a86a1f6ce86c449c46666bda44268677abf29b5b2d2eb5ec7af903ec2f117a82 + bcec67e8e99cabfa7764d75ad9b158d72bfacf70ca1d0ec8bc6b4406d1bf8413 + c00aae8461a256275598500ea0e187588c35a5d5d7454fb57eac18d9edb86a56 + d4a35cd3121aa00d18544bb0ac01c3e1691d618f462c46129271bccf39f7e8ee +``` +and run `cabal update`. + +[`@haskell`]: https://flora.pm/packages/@haskell +[`@cardano`]: https://flora.pm/packages/@cardano +[`@hackage`]: https://flora.pm/packages/@hackage +[`@hackage/servant-server`]: https://flora.pm/packages/@hackage/servant-server +[`@haskell/text`]: https://flora.pm/packages/@haskell/text +[CHaP]: https://input-output-hk.github.io/cardano-haskell-packages From 1d5c75259e7218e9e9d74c1ea30a9d625c7440c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:08:30 +0200 Subject: [PATCH 15/40] Bump @babel/traverse from 7.22.19 to 7.23.2 in /design (#462) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- design/package-lock.json | 120 +++++++++++++++++++-------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/design/package-lock.json b/design/package-lock.json index bcfc5f8b..6f30c5fd 100644 --- a/design/package-lock.json +++ b/design/package-lock.json @@ -100,12 +100,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", - "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.22.15", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -211,22 +211,22 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -388,9 +388,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.19.tgz", - "integrity": "sha512-Tinq7ybnEPFFXhlYOYFiSjespWQk0dq2dRNAiMdRTOYQzEGqnnNyrTxPYHP5r6wGjlF1rFgABdDV0g8EwD6Qbg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -448,9 +448,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.16", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", - "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1946,19 +1946,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.19.tgz", - "integrity": "sha512-ZCcpVPK64krfdScRbpxF6xA5fz7IOsfMwx1tcACvCzt6JY+0aHkBk7eIU8FRDSZRU5Zei6Z4JfgAxN1bqXGECg==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.16", - "@babel/types": "^7.22.19", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1967,13 +1967,13 @@ } }, "node_modules/@babel/types": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", - "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.19", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -12517,12 +12517,12 @@ } }, "@babel/generator": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", - "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "requires": { - "@babel/types": "^7.22.15", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -12601,19 +12601,19 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { @@ -12727,9 +12727,9 @@ "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.19.tgz", - "integrity": "sha512-Tinq7ybnEPFFXhlYOYFiSjespWQk0dq2dRNAiMdRTOYQzEGqnnNyrTxPYHP5r6wGjlF1rFgABdDV0g8EwD6Qbg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { @@ -12772,9 +12772,9 @@ } }, "@babel/parser": { - "version": "7.22.16", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", - "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { @@ -13775,31 +13775,31 @@ } }, "@babel/traverse": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.19.tgz", - "integrity": "sha512-ZCcpVPK64krfdScRbpxF6xA5fz7IOsfMwx1tcACvCzt6JY+0aHkBk7eIU8FRDSZRU5Zei6Z4JfgAxN1bqXGECg==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", "dev": true, "requires": { "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.22.15", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.16", - "@babel/types": "^7.22.19", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", - "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "requires": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.19", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, From 6adc0fbf9ad10f596b35b9808c427ffc9783bc62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:25:11 +0200 Subject: [PATCH 16/40] Bump @babel/traverse from 7.22.8 to 7.23.2 in /docs (#463) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/yarn.lock | 92 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index 42d7eaa5..ae68107a 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -153,6 +153,14 @@ dependencies: "@babel/highlight" "^7.22.5" +"@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + "@babel/compat-data@^7.22.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9": version "7.22.9" resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.22.9.tgz#71cdb00a1ce3a329ce4cbec3a44f9fef35669730" @@ -201,7 +209,7 @@ json5 "^2.2.2" semver "^6.3.1" -"@babel/generator@^7.12.5", "@babel/generator@^7.18.7", "@babel/generator@^7.22.7", "@babel/generator@^7.22.9": +"@babel/generator@^7.12.5", "@babel/generator@^7.18.7", "@babel/generator@^7.22.9": version "7.22.9" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.22.9.tgz#572ecfa7a31002fa1de2a9d91621fd895da8493d" integrity sha512-KtLMbmicyuK2Ak/FTCJVbDnkN1SlT8/kceFTiuDiiRUUSMnHMidxSCdG4ndkTOHHpoomWe/4xkvHkEOncwjYIw== @@ -211,6 +219,16 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + "@babel/helper-annotate-as-pure@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" @@ -271,6 +289,11 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + "@babel/helper-environment-visitor@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz#f06dd41b7c1f44e1f8da6c4055b41ab3a09a7e98" @@ -284,6 +307,14 @@ "@babel/template" "^7.22.5" "@babel/types" "^7.22.5" +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + "@babel/helper-hoist-variables@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" @@ -377,6 +408,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + "@babel/helper-validator-identifier@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz#9544ef6a33999343c8740fa51350f30eeaaaf193" @@ -405,6 +441,15 @@ "@babel/traverse" "^7.22.6" "@babel/types" "^7.22.5" +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + "@babel/highlight@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" @@ -419,6 +464,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae" integrity sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q== +"@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz#87245a21cd69a73b0b81bcda98d443d6df08f05e" @@ -1204,19 +1254,28 @@ "@babel/parser" "^7.22.5" "@babel/types" "^7.22.5" -"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.4.5": - version "7.22.8" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.8.tgz#4d4451d31bc34efeae01eac222b514a77aa4000e" - integrity sha512-y6LPR+wpM2I3qJrsheCTwhIinzkETbplIgPBbwvqPKc+uljeA5gP+3nP8irdYt1mjQaDnlIcG+dw8OjAco4GXw== +"@babel/template@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== dependencies: - "@babel/code-frame" "^7.22.5" - "@babel/generator" "^7.22.7" - "@babel/helper-environment-visitor" "^7.22.5" - "@babel/helper-function-name" "^7.22.5" + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/traverse@^7.12.9", "@babel/traverse@^7.18.8", "@babel/traverse@^7.22.6", "@babel/traverse@^7.22.8", "@babel/traverse@^7.4.5": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.22.7" - "@babel/types" "^7.22.5" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" debug "^4.1.0" globals "^11.1.0" @@ -1229,6 +1288,15 @@ "@babel/helper-validator-identifier" "^7.22.5" to-fast-properties "^2.0.0" +"@babel/types@^7.22.15", "@babel/types@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -3069,7 +3137,7 @@ ccount@^1.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== -chalk@^2.0.0: +chalk@^2.0.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== From bb11a772663160d09b4a61a6fcdfd3353152eab6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 09:16:11 +0200 Subject: [PATCH 17/40] Bump actions/setup-node from 3 to 4 (#468) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/backend.yml | 2 +- .github/workflows/frontend.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 9759dfd3..641d4a76 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -57,7 +57,7 @@ jobs: ghc-version: "${{ matrix.ghc }}" cabal-version: "latest" - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18" cache: "yarn" diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 99c64aff..84f08c53 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "18" cache: "yarn" From 7f87bbc1901f664759911c121629b1dee66c8c95 Mon Sep 17 00:00:00 2001 From: Raoul Hidalgo Charman Date: Wed, 25 Oct 2023 23:53:35 +0700 Subject: [PATCH 18/40] [FLORA-7] Tarball support (#452) --- CHANGELOG.md | 1 + app/cli/Main.hs | 54 +++++- flora.cabal | 23 ++- migrations/20230809181650_create_binary.sql | 12 ++ src/core/Flora/Environment.hs | 25 +++ src/core/Flora/Environment/Config.hs | 23 +++ src/core/Flora/Import/Package.hs | 2 + src/core/Flora/Model/BlobIndex/Internal.hs | 174 ++++++++++++++++++ src/core/Flora/Model/BlobIndex/Query.hs | 72 ++++++++ src/core/Flora/Model/BlobIndex/Types.hs | 78 ++++++++ src/core/Flora/Model/BlobIndex/Update.hs | 80 ++++++++ src/core/Flora/Model/BlobStore/API.hs | 85 +++++++++ src/core/Flora/Model/BlobStore/Types.hs | 38 ++++ src/core/Flora/Model/Job.hs | 9 + src/core/Flora/Model/Package/Types.hs | 7 + src/core/Flora/Model/Release/Query.hs | 52 ++++-- src/core/Flora/Model/Release/Types.hs | 3 + src/core/Flora/Model/Release/Update.hs | 32 +++- .../Database/PostgreSQL/Simple/Orphans.hs | 9 + src/datatypes/Distribution/Orphans/Version.hs | 5 + .../Servant/API/ContentTypes/GZip.hs | 20 ++ src/jobs-worker/FloraJobs/Runner.hs | 47 +++++ src/jobs-worker/FloraJobs/Scheduler.hs | 61 ++---- .../FloraJobs/ThirdParties/Hackage/API.hs | 7 + .../FloraJobs/ThirdParties/Hackage/Client.hs | 14 +- src/jobs-worker/FloraJobs/Types.hs | 21 ++- src/web/FloraWeb/Common/Auth/Types.hs | 6 + src/web/FloraWeb/Pages/Routes/Packages.hs | 10 + src/web/FloraWeb/Pages/Server/Admin.hs | 20 +- src/web/FloraWeb/Pages/Server/Packages.hs | 26 ++- src/web/FloraWeb/Pages/Templates/Error.hs | 2 + src/web/FloraWeb/Pages/Templates/Packages.hs | 17 +- .../Pages/Templates/Pages/Packages.hs | 2 +- src/web/FloraWeb/Pages/Templates/Types.hs | 13 +- src/web/FloraWeb/Server.hs | 16 +- src/web/FloraWeb/Types.hs | 5 + test/Flora/BlobSpec.hs | 100 ++++++++++ test/Flora/ImportSpec.hs | 2 +- test/Flora/PackageSpec.hs | 2 +- test/Flora/TestUtils.hs | 4 +- test/Main.hs | 5 + test/fixtures/Cabal/bad-tar.cabal | 17 ++ test/fixtures/Cabal/malformed-tar.cabal | 17 ++ .../Tarball/tar-a/0.1.0.0/tar-a.cabal | 35 ---- .../Tarball/tar-b/0.1.0.0/tar-b.cabal | 40 ---- test/fixtures/tarballs/b-0.1.0.0.tar.gz | Bin 0 -> 695 bytes test/fixtures/tarballs/bad-tar-0.1.0.0.tar.gz | Bin 0 -> 253 bytes .../tarballs/malformed-tar-0.1.0.0.tar.gz | Bin 0 -> 770 bytes .../fixtures/{ => tarballs}/test-index.tar.gz | Bin 49 files changed, 1131 insertions(+), 162 deletions(-) create mode 100644 migrations/20230809181650_create_binary.sql create mode 100644 src/core/Flora/Model/BlobIndex/Internal.hs create mode 100644 src/core/Flora/Model/BlobIndex/Query.hs create mode 100644 src/core/Flora/Model/BlobIndex/Types.hs create mode 100644 src/core/Flora/Model/BlobIndex/Update.hs create mode 100644 src/core/Flora/Model/BlobStore/API.hs create mode 100644 src/core/Flora/Model/BlobStore/Types.hs create mode 100644 src/datatypes/Database/PostgreSQL/Simple/Orphans.hs create mode 100644 src/datatypes/Servant/API/ContentTypes/GZip.hs create mode 100644 test/Flora/BlobSpec.hs create mode 100644 test/fixtures/Cabal/bad-tar.cabal create mode 100644 test/fixtures/Cabal/malformed-tar.cabal delete mode 100644 test/fixtures/Tarball/tar-a/0.1.0.0/tar-a.cabal delete mode 100644 test/fixtures/Tarball/tar-b/0.1.0.0/tar-b.cabal create mode 100644 test/fixtures/tarballs/b-0.1.0.0.tar.gz create mode 100644 test/fixtures/tarballs/bad-tar-0.1.0.0.tar.gz create mode 100644 test/fixtures/tarballs/malformed-tar-0.1.0.0.tar.gz rename test/fixtures/{ => tarballs}/test-index.tar.gz (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fddb66e..533e9d63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Colourise in red deprecation markers on the package page ([#438](/~https://github.com/flora-pm/flora-server/pull/439)) * Allow package imports from multiple repositories ([#444](/~https://github.com/flora-pm/flora-server/pull/444)) * Add a page on namespaces in the documentation ([#451](/~https://github.com/flora-pm/flora-server/pull/451)) +* Add initial support for hosting package tarballs ([#452](/~https://github.com/flora-pm/flora-server/pull/452)) ## 1.0.13 -- 2023-09-17 * Exclude deprecated releases from latest versions and search ([#373](/~https://github.com/flora-pm/flora-server/pull/373)) diff --git a/app/cli/Main.hs b/app/cli/Main.hs index 0fdd3528..29d56685 100644 --- a/app/cli/Main.hs +++ b/app/cli/Main.hs @@ -1,15 +1,22 @@ module Main where +import Codec.Compression.GZip qualified as GZip +import Data.ByteString.Lazy.Char8 qualified as BS import Data.Maybe import Data.Password.Types import Data.Text (Text) import Data.Text qualified as Text +import Data.Text.Display (display) import DesignSystem (generateComponents) +import Distribution.Version (Version) import Effectful import Effectful.Fail +import Effectful.Log import Effectful.PostgreSQL.Transact.Effect import Effectful.Reader.Static (Reader, runReader) +import Effectful.Time (Time, runTime) import GHC.Generics (Generic) +import Log qualified import Log.Backend.StandardOutput qualified as Log import Optics.Core import Options.Applicative @@ -18,6 +25,9 @@ import Flora.Environment import Flora.Environment.Config (PoolConfig (..)) import Flora.Import.Categories (importCategories) import Flora.Import.Package.Bulk (importAllFilesInRelativeDirectory, importFromIndex) +import Flora.Model.BlobIndex.Update qualified as Update +import Flora.Model.BlobStore.API +import Flora.Model.Package (PackageName) import Flora.Model.PackageIndex.Query qualified as Query import Flora.Model.PackageIndex.Types import Flora.Model.PackageIndex.Update qualified as Update @@ -37,6 +47,7 @@ data Command | ImportPackages FilePath Text | ImportIndex FilePath Text | ProvisionRepository Text Text + | ImportPackageTarball PackageName Version FilePath deriving stock (Show, Eq) data ProvisionTarget @@ -61,6 +72,11 @@ main = do . runReader env.dbConfig . runDB env.pool . runFailIO + . runTime + . ( case env.features.blobStoreImpl of + Just (BlobStoreFS fp) -> runBlobStoreFS fp + _ -> runBlobStorePure + ) $ runOptions result parseOptions :: Parser Options @@ -76,6 +92,11 @@ parseCommand = <> command "import-packages" (parseImportPackages `withInfo` "Import cabal packages from a directory") <> command "import-index" (parseImportIndex `withInfo` "Import cabal packages from the index tarball") <> command "provision-repository" (parseProvisionRepository `withInfo` "Create a package repository") + <> command + "import-package-tarball" + ( parseImportPackageTarball + `withInfo` "Import a single package tarball, useful for testing" + ) parseProvision :: Parser Command parseProvision = @@ -115,7 +136,23 @@ parseProvisionRepository = <$> option str (long "name" <> metavar "" <> help "Name of the repository") <*> option str (long "url" <> metavar "" <> help "Link to the package repository") -runOptions :: (Reader PoolConfig :> es, DB :> es, Fail :> es, IOE :> es) => Options -> Eff es () +parseImportPackageTarball :: Parser Command +parseImportPackageTarball = + ImportPackageTarball + <$> argument str (metavar "PACKAGE_NAME") + <*> argument str (metavar "VERSION") + <*> argument str (metavar "PATH") + +runOptions + :: ( Reader PoolConfig :> es + , DB :> es + , Time :> es + , Fail :> es + , IOE :> es + , BlobStoreAPI :> es + ) + => Options + -> Eff es () runOptions (Options (Provision Categories)) = importCategories runOptions (Options (Provision TestPackages)) = importFolderOfCabalFiles "./test/fixtures/Cabal/" "hackage" runOptions (Options (CreateUser opts)) = do @@ -138,6 +175,7 @@ runOptions (Options GenDesignSystemComponents) = generateComponents runOptions (Options (ImportPackages path repository)) = importFolderOfCabalFiles path repository runOptions (Options (ImportIndex path repository)) = importIndex path repository runOptions (Options (ProvisionRepository name url)) = provisionRepository name url +runOptions (Options (ImportPackageTarball pname version path)) = importPackageTarball pname version path provisionRepository :: (DB :> es, IOE :> es) => Text -> Text -> Eff es () provisionRepository name url = do @@ -161,5 +199,19 @@ importIndex path repository = Log.withStdOutLogger $ \logger -> do Just packageIndex -> importFromIndex logger (user ^. #userId) (repository, packageIndex.url) path True +importPackageTarball + :: (BlobStoreAPI :> es, Time :> es, IOE :> es, DB :> es) + => PackageName + -> Version + -> FilePath + -> Eff es () +importPackageTarball pname version path = Log.withStdOutLogger $ \logger -> do + contents <- liftIO $ GZip.decompress <$> BS.readFile path + runLog "flora-cli" logger Log.LogTrace $ do + res <- Update.insertTar pname version contents + case res of + Right hash -> Log.logInfo_ $ "Insert tarball with root hash: " <> display hash + Left err -> Log.logAttention_ $ display err + withInfo :: Parser a -> String -> ParserInfo a withInfo opts desc = info (helper <*> opts) $ progDesc desc diff --git a/flora.cabal b/flora.cabal index c4565dd3..2729aa2f 100644 --- a/flora.cabal +++ b/flora.cabal @@ -81,6 +81,7 @@ library Data.Positive Data.Text.Display.Orphans Data.Time.Orphans + Database.PostgreSQL.Simple.Orphans Distribution.Orphans Distribution.Orphans.CompilerFlavor Distribution.Orphans.ConfVar @@ -96,6 +97,12 @@ library Flora.Import.Types Flora.Logging Flora.Model.Admin.Report + Flora.Model.BlobIndex.Internal + Flora.Model.BlobIndex.Query + Flora.Model.BlobIndex.Types + Flora.Model.BlobIndex.Update + Flora.Model.BlobStore.API + Flora.Model.BlobStore.Types Flora.Model.Category Flora.Model.Category.Query Flora.Model.Category.Types @@ -129,10 +136,12 @@ library JSON Log.Backend.File Lucid.Orphans + Servant.API.ContentTypes.GZip build-depends: , aeson , base ^>=4.17 + , base16-bytestring , base64 , blaze-builder , bytestring @@ -140,6 +149,7 @@ library , colourista , containers , cryptohash-md5 + , cryptohash-sha256 , cryptonite , cryptonite-conduit , deepseq @@ -149,6 +159,7 @@ library , envparse , filepath , http-api-data + , http-media , iso8601-time , log-base , log-effectful @@ -169,6 +180,7 @@ library , postgresql-simple , pretty , resource-pool + , servant , servant-lucid , servant-server , slugify @@ -180,6 +192,7 @@ library , text-display , time , unliftio + , utf8-string , uuid , vector , vector-algorithms @@ -363,7 +376,6 @@ library flora-jobs , text , text-display , time - , typed-process , vector executable flora-server @@ -392,16 +404,20 @@ executable flora-cli , flora , flora-web , log-base + , log-effectful , lucid + , monad-time-effectful , optics-core , optparse-applicative , password-types , pg-transact-effectful , PyF , text + , text-display , transformers , uuid , vector + , zlib test-suite flora-test import: common-extensions @@ -413,10 +429,12 @@ test-suite flora-test build-depends: , aeson , base + , bytestring , Cabal-syntax , containers , effectful-core , exceptions + , filepath , flora , flora-web , hedgehog @@ -435,6 +453,7 @@ test-suite flora-test , servant , servant-client , servant-server + , tar , tasty , tasty-hunit , text @@ -442,8 +461,10 @@ test-suite flora-test , transformers , uuid , vector + , zlib other-modules: + Flora.BlobSpec Flora.CabalSpec Flora.CategorySpec Flora.ImportSpec diff --git a/migrations/20230809181650_create_binary.sql b/migrations/20230809181650_create_binary.sql new file mode 100644 index 00000000..8d883aa0 --- /dev/null +++ b/migrations/20230809181650_create_binary.sql @@ -0,0 +1,12 @@ +create table if not exists blob_relations ( + blob_hash text not null, + blob_dep_hash text not null, + blob_dep_path text not null, + blob_dep_directory bool not null, + + constraint pk_relation primary key (blob_hash, blob_dep_path) +); + +-- Just points to the hash of the root directory +alter table releases add tarball_root_hash text; +alter table releases add tarball_archive_hash text; diff --git a/src/core/Flora/Environment.hs b/src/core/Flora/Environment.hs index 92498dcc..98e23bd4 100644 --- a/src/core/Flora/Environment.hs +++ b/src/core/Flora/Environment.hs @@ -4,6 +4,8 @@ module Flora.Environment ( FloraEnv (..) , DeploymentEnv (..) , LoggingEnv (..) + , FeatureEnv (..) + , BlobStoreImpl (..) , TestEnv (..) , getFloraEnv , getFloraTestEnv @@ -11,6 +13,7 @@ module Flora.Environment where import Colourista.IO (blueMessage) +import Data.Aeson (ToJSON) import Data.ByteString (ByteString) import Data.Pool (Pool) import Data.Pool qualified as Pool @@ -37,6 +40,7 @@ data FloraEnv = FloraEnv , domain :: Text , logging :: LoggingEnv , environment :: DeploymentEnv + , features :: FeatureEnv , config :: FloraConfig , assets :: Assets } @@ -64,6 +68,25 @@ mkPool connectionInfo timeout' connections = (realToFrac timeout') connections +data BlobStoreImpl = BlobStoreFS FilePath | BlobStorePure + deriving stock (Generic, Show) + +instance ToJSON BlobStoreImpl + +newtype FeatureEnv = FeatureEnv {blobStoreImpl :: Maybe BlobStoreImpl} + deriving stock (Generic, Show) + +instance ToJSON FeatureEnv + +-- In future we'll want to error for conflicting options +featureConfigToEnv :: FeatureConfig -> Eff es FeatureEnv +featureConfigToEnv FeatureConfig{..} = + case blobStoreFS of + Just fp | tarballsEnabled -> pure . FeatureEnv . Just $ BlobStoreFS fp + _ -> + pure . FeatureEnv $ + if tarballsEnabled then Just BlobStorePure else Nothing + configToEnv :: (Fail :> es, IOE :> es) => FloraConfig -> Eff es FloraEnv configToEnv floraConfig = do let PoolConfig{connectionTimeout, connections} = floraConfig.dbConfig @@ -71,6 +94,7 @@ configToEnv floraConfig = do jobsPool <- mkPool floraConfig.connectionInfo connectionTimeout connections assets <- getAssets floraConfig.environment liftIO $ print assets + featureEnv <- featureConfigToEnv floraConfig.features pure FloraEnv { pool = pool @@ -80,6 +104,7 @@ configToEnv floraConfig = do , domain = floraConfig.domain , logging = floraConfig.logging , environment = floraConfig.environment + , features = featureEnv , assets = assets , config = floraConfig } diff --git a/src/core/Flora/Environment/Config.hs b/src/core/Flora/Environment/Config.hs index 79928ab5..5cd36aea 100644 --- a/src/core/Flora/Environment/Config.hs +++ b/src/core/Flora/Environment/Config.hs @@ -2,6 +2,7 @@ module Flora.Environment.Config ( FloraConfig (..) , LoggingEnv (..) + , FeatureConfig (..) , ConnectionInfo (..) , TestConfig (..) , PoolConfig (..) @@ -43,12 +44,14 @@ import Env , def , help , nonempty + , optional , str , switch , var , (<=<) ) import GHC.Generics (Generic) +import System.FilePath (isValid) import Text.Read (readMaybe) data ConnectionInfo = ConnectionInfo @@ -100,6 +103,12 @@ data LoggingEnv = LoggingEnv } deriving stock (Show, Generic) +data FeatureConfig = FeatureConfig + { tarballsEnabled :: Bool + , blobStoreFS :: Maybe FilePath + } + deriving stock (Show, Generic) + -- | The datatype that is used to model the external configuration data FloraConfig = FloraConfig { dbConfig :: PoolConfig @@ -107,6 +116,7 @@ data FloraConfig = FloraConfig , domain :: Text , httpPort :: Word16 , logging :: LoggingEnv + , features :: FeatureConfig , environment :: DeploymentEnv } deriving stock (Show, Generic) @@ -144,6 +154,15 @@ parseLoggingEnv = <*> switch "FLORA_PROMETHEUS_ENABLED" (help "Whether or not Prometheus is enabled") <*> var loggingDestination "FLORA_LOGGING_DESTINATION" (help "Where do the logs go") +parseFeatures :: Parser Error FeatureConfig +parseFeatures = + FeatureConfig + <$> switch "FLORA_TARBALLS_ENABLED" (help "Whether to store package tarballs, by default off for now") + <*> optional + ( var filepath "FLORA_TARBALLS_FS_PATH" $ + help "Store tarball blobs in the supplied filesystem directory" + ) + parsePort :: Parser Error Word16 parsePort = var port "FLORA_HTTP_PORT" (help "HTTP Port for Flora") @@ -162,6 +181,7 @@ parseConfig = <*> parseDomain <*> parsePort <*> parseLoggingEnv + <*> parseFeatures <*> parseDeploymentEnv parseTestConfig :: Parser Error TestConfig @@ -204,6 +224,9 @@ loggingDestination "json" = Right Json loggingDestination "json-file" = Right JSONFile loggingDestination e = Left $ unread e +filepath :: Reader Error FilePath +filepath fp = if isValid fp then Right fp else Left $ unread fp + getAssets :: (Fail :> es, IOE :> es) => DeploymentEnv -> Eff es Assets getAssets environment = case environment of diff --git a/src/core/Flora/Import/Package.hs b/src/core/Flora/Import/Package.hs index 9d17ae1a..acae3368 100644 --- a/src/core/Flora/Import/Package.hs +++ b/src/core/Flora/Import/Package.hs @@ -352,6 +352,8 @@ extractPackageDataFromCabal userId (repositoryName, repositoryPackages) uploadTi , changelog = Nothing , changelogStatus = NotImported , repository = Just repositoryName + , tarballRootHash = Nothing + , tarballArchiveHash = Nothing , license = Cabal.license packageDesc , sourceRepos , homepage = Just $ display packageDesc.homepage diff --git a/src/core/Flora/Model/BlobIndex/Internal.hs b/src/core/Flora/Model/BlobIndex/Internal.hs new file mode 100644 index 00000000..c4a9a1ed --- /dev/null +++ b/src/core/Flora/Model/BlobIndex/Internal.hs @@ -0,0 +1,174 @@ +{-# LANGUAGE OverloadedLists #-} + +module Flora.Model.BlobIndex.Internal + ( TarError + , Sha256Sum + , TarRoot (..) + , TarTree (..) + , tarballToTree + , treeToTarball + , hashTree + ) +where + +import Data.Aeson (ToJSON (..), object) +import Data.ByteString (StrictByteString) +import Data.ByteString qualified as BS +import Data.ByteString.Lazy (LazyByteString) +import Data.ByteString.UTF8 qualified as BSU +import Data.List (foldl') +import Data.Map qualified as M +import Data.Text qualified as T +import Data.Text.Display (display) +import Log ((.=)) +import System.FilePath (dropTrailingPathSeparator, joinPath, splitPath, ()) + +import Codec.Archive.Tar qualified as Tar +import Codec.Archive.Tar.Entry qualified as Tar +import Crypto.Hash.SHA256 qualified as SHA +import Distribution.Version (Version) + +import Flora.Model.BlobIndex.Types (TarError (..)) +import Flora.Model.BlobStore.API (hashByteString) +import Flora.Model.BlobStore.Types (Sha256Sum (..)) +import Flora.Model.Package (PackageName) + +-- | Structure for representing a tarball directory tree +data TarTree a + = TarDirectory a (M.Map FilePath (TarTree a)) + | TarFile a StrictByteString + deriving (Eq, Show) + +ann :: TarTree a -> a +ann (TarDirectory a _) = a +ann (TarFile a _) = a + +instance ToJSON a => ToJSON (TarTree a) where + toJSON (TarDirectory a nodes) = + object ["ann" .= a, "nodes" .= nodes] + toJSON (TarFile a _content) = + object ["ann" .= a] + +-- | Root directory structure +-- +-- We expect everything contained within a directory of "{pname}-{version}" +-- anything outside of that is a malformed tarball we shouldn't accept +data TarRoot a = TarRoot a PackageName Version (M.Map FilePath (TarTree a)) + deriving (Eq, Show) + +instance ToJSON a => ToJSON (TarRoot a) where + toJSON (TarRoot a pname version tree) = + object + [ "ann" .= a + , "packageName" .= pname + , "version" .= version + , "tree" .= tree + ] + +-- | Aux function for finding where to put one node in the tree +-- +-- Provided the entry has the expected root directory all other directories will +-- be created if not found +insertTarContents + :: [FilePath] -> TarTree () -> TarRoot () -> Either TarError (TarRoot ()) +insertTarContents dirs content (TarRoot () pname version tree) = case dirs of + x : xs -> TarRoot () pname version <$> M.alterF (go xs) x tree + _ -> Left TarEmpty + where + go [] Nothing = Right $ Just content + -- Sometimes directories are specified in different orders so we should leave + -- them as is, they may have had entries added we don't want to remove + go [] (Just t@TarDirectory{}) = Right $ Just t + -- Files are also occasionally duplicated so we should overwrite it like a tarball + -- extraction would do + go [] (Just TarFile{}) = Right $ Just content + go (x : xs) Nothing = Just . TarDirectory () <$> M.alterF (go xs) x M.empty + go (x : xs) (Just (TarDirectory () nodes)) = Just . TarDirectory () <$> M.alterF (go xs) x nodes + go _ (Just TarFile{}) = Left $ TarCouldntInsert $ joinPath dirs + +-- First we construct a directory tree from a tarball +-- This makes it easier to create merkle trees later, and gives us a check for +-- conflicts +tarballToTree :: PackageName -> Version -> LazyByteString -> Either TarError (TarRoot ()) +tarballToTree pname version = + either (Left . TarFormatError . fst) id + . checkAndFold + . Tar.read + where + root = TarRoot () pname version M.empty + rootdir = T.unpack $ display pname <> "-" <> display version + sanitisedTarPaths = fmap dropTrailingPathSeparator . splitPath . Tar.entryPath + + -- If we start with just the root directory we want to skip over it + checkAndFold t@(Tar.Next e es) = + Tar.foldlEntries go (Right root) $ + if sanitisedTarPaths e == [rootdir] then es else t + checkAndFold Tar.Done = Right $ Left TarEmpty + checkAndFold (Tar.Fail err) = Left (err, Right root) + + -- Insert each tar entry into our tree with a check for anything outside the root dir + go (Left err) _ = Left err + go (Right acc) entry + | head dirs /= rootdir = Left $ TarUnexpectedLayout $ joinPath dirs + | otherwise = case Tar.entryContent entry of + Tar.NormalFile bs _ -> + insertTarContents + (drop 1 dirs) + (TarFile () $ BS.toStrict bs) + acc + Tar.Directory -> + insertTarContents + (drop 1 dirs) + (TarDirectory () M.empty) + acc + u -> Left $ TarUnsupportedEntry u + where + dirs = sanitisedTarPaths entry + +-- Traverse over directory tree and create tarball contents +treeToTarball :: TarRoot a -> LazyByteString +treeToTarball (TarRoot _ pname version tree) = + Tar.write $ + Tar.directoryEntry + (toTarPath True rootdir) + : concatMap + (\(fp, node) -> nodeToEntries (rootdir fp) node) + (M.toList tree) + where + toTarPath b dir = + either (\fp -> error $ "Directory path too long: " <> fp) id $ + Tar.toTarPath b dir + rootdir = T.unpack $ display pname <> "-" <> display version + nodeToEntries dir = \case + TarDirectory _ nodes -> + Tar.directoryEntry + (toTarPath True dir) + : concatMap (\(fp, node) -> nodeToEntries (dir fp) node) (M.toList nodes) + TarFile _ contents -> + pure $ Tar.fileEntry (toTarPath False dir) $ BS.fromStrict contents + +-- | Tag the tree with the sha256sum hashes +-- +-- Hashed by contents for files, by listed hashes and filepaths for directories +hashTree :: TarRoot () -> TarRoot Sha256Sum +hashTree (TarRoot _ pname version tree) = + let tree' = go <$> tree + in TarRoot (toHash tree') pname version tree' + where + go (TarFile _ content) = + let hash = hashByteString content + in TarFile hash content + go (TarDirectory _ nodes) = + let nodes' = go <$> nodes + in TarDirectory (toHash nodes') nodes' + toHash = + Sha256Sum + . SHA.finalize + . foldl' SHA.update SHA.init + . concatMap + ( \(fp, entry) -> + [ bytestring (ann entry) + , BSU.fromString fp + ] + ) + . M.toList diff --git a/src/core/Flora/Model/BlobIndex/Query.hs b/src/core/Flora/Model/BlobIndex/Query.hs new file mode 100644 index 00000000..958b2c89 --- /dev/null +++ b/src/core/Flora/Model/BlobIndex/Query.hs @@ -0,0 +1,72 @@ +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE QuasiQuotes #-} + +module Flora.Model.BlobIndex.Query + ( queryTar + ) where + +import Control.Exception (throw) +import Data.ByteString.Lazy (LazyByteString) +import Data.Map qualified as M +import Data.Text.Display (display) +import Data.Vector qualified as V + +import Database.PostgreSQL.Entity (_orderBy, _selectWhere) +import Database.PostgreSQL.Entity.DBT (QueryNature (..), query) +import Database.PostgreSQL.Entity.Types (SortKeyword (..), field) +import Database.PostgreSQL.Simple (Only (..)) +import Effectful (Eff, type (:>)) +import Effectful.Log (Log) +import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff) +import Log qualified + +import Distribution.Version (Version) +import Flora.Model.BlobIndex.Types (BlobRelation (..), BlobStoreQueryError (..)) +import Flora.Model.BlobStore.API (BlobStoreAPI, get) +import Flora.Model.Package (PackageName) + +import Flora.Model.BlobIndex.Internal + +-- | Query a package name, version and hash and construct a uncompressed tarball +-- from the database +queryTar + :: forall es + . (Log :> es, DB :> es, BlobStoreAPI :> es) + => PackageName + -> Version + -> Sha256Sum + -> Eff es LazyByteString +queryTar pname version rootHash = do + Log.logInfo_ $ "Querying for " <> display rootHash + children <- traverse go =<< queryChildren rootHash + let tree = TarRoot rootHash pname version . M.fromList $ V.toList children + pure $ treeToTarball tree + where + queryChildren :: Sha256Sum -> Eff es (V.Vector BlobRelation) + queryChildren hash = + dbtToEff $! + query + Select + ( _selectWhere @BlobRelation [[field| blob_hash |]] + -- Ensures we consistently get back the directory structure + -- This may not be the same as the hackage tarball! + <> _orderBy ([field| blob_dep_path |], ASC) + ) + (Only hash) + go :: BlobRelation -> Eff es (FilePath, TarTree Sha256Sum) + go BlobRelation{..} + | blobDepDirectory = + (blobDepPath,) + . TarDirectory blobDepHash + . M.fromList + . V.toList + <$> (traverse go =<< queryChildren blobDepHash) + go BlobRelation{..} = do + mcontent <- get blobDepHash + case mcontent of + Nothing -> throw $ IncompleteDirectoryTree pname version + Just bytes -> + pure + ( blobDepPath + , TarFile blobDepHash bytes + ) diff --git a/src/core/Flora/Model/BlobIndex/Types.hs b/src/core/Flora/Model/BlobIndex/Types.hs new file mode 100644 index 00000000..398424f2 --- /dev/null +++ b/src/core/Flora/Model/BlobIndex/Types.hs @@ -0,0 +1,78 @@ +module Flora.Model.BlobIndex.Types where + +import Control.DeepSeq (NFData) +import Control.Exception (Exception) +import Data.Aeson.Orphans () +import Data.Text.Display (Display (..)) +import GHC.Generics (Generic) + +import Codec.Archive.Tar qualified as Tar +import Database.PostgreSQL.Entity.Types (Entity, GenericEntity, TableName) +import Database.PostgreSQL.Simple.FromRow (FromRow (..)) +import Database.PostgreSQL.Simple.Orphans () +import Database.PostgreSQL.Simple.ToRow (ToRow (..)) +import Distribution.Version (Version) + +import Flora.Model.BlobStore.Types (Sha256Sum (..)) +import Flora.Model.Package.Types (PackageName) + +data TarError + = TarFormatError Tar.FormatError + | TarUnsupportedEntry Tar.EntryContent + | TarUnexpectedLayout FilePath + | TarEmpty + | TarCouldntInsert FilePath + deriving stock (Eq, Show) + deriving anyclass (Exception) + +data BlobStoreQueryError + = IncompleteDirectoryTree PackageName Version + deriving stock (Eq, Show) + deriving anyclass (Exception) + +data BlobStoreInsertError + = NoPackage PackageName + | NoRelease PackageName Version + | BlobStoreTarError PackageName Version TarError + deriving stock (Eq, Show) + deriving anyclass (Exception) + +instance Display BlobStoreInsertError where + displayBuilder = \case + NoPackage pname -> + "Couldn't find package " <> displayBuilder pname + NoRelease pname version -> + "Couldn't find release " + <> displayBuilder pname + <> "-" + <> displayBuilder version + BlobStoreTarError pname version err -> + "Tarball issue with release " + <> displayBuilder pname + <> "-" + <> displayBuilder version + <> ": " + <> displayBuilder (show err) + +data BlobRelation = BlobRelation + { blobHash :: Sha256Sum + , blobDepHash :: Sha256Sum + , blobDepPath :: FilePath + , blobDepDirectory :: Bool + } + deriving (Generic, NFData) + deriving (FromRow, ToRow) + deriving + (Entity) + via (GenericEntity '[TableName "blob_relations"] BlobRelation) + +instance Display BlobRelation where + displayBuilder (BlobRelation hash depHash depPath depDirectory) = + "BlobRelation " + <> displayBuilder hash + <> " " + <> displayBuilder depHash + <> " " + <> displayBuilder depPath + <> " " + <> displayBuilder depDirectory diff --git a/src/core/Flora/Model/BlobIndex/Update.hs b/src/core/Flora/Model/BlobIndex/Update.hs new file mode 100644 index 00000000..c73956a7 --- /dev/null +++ b/src/core/Flora/Model/BlobIndex/Update.hs @@ -0,0 +1,80 @@ +module Flora.Model.BlobIndex.Update where + +import Control.Monad (void, when) +import Control.Monad.IO.Class (MonadIO) +import Data.ByteString.Lazy (LazyByteString) +import Data.Int (Int64) +import Data.Map qualified as M +import Data.String (fromString) +import Data.Text.Display (display) +import Effectful (Eff, type (:>)) +import Effectful.Log (Log) +import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff) +import Effectful.Time (Time) +import Log qualified + +import Database.PostgreSQL.Entity (Entity, _insert) +import Database.PostgreSQL.Entity.DBT (QueryNature (..), execute) +import Database.PostgreSQL.Simple (ToRow) +import Database.PostgreSQL.Simple.Types (Query) +import Database.PostgreSQL.Transact (DBT) + +import Distribution.Version (Version) + +import Flora.Model.BlobIndex.Internal +import Flora.Model.BlobIndex.Types +import Flora.Model.BlobStore.API +import Flora.Model.Package.Query qualified as Query +import Flora.Model.Package.Types +import Flora.Model.Release.Query qualified as Query +import Flora.Model.Release.Types (Release (..), ReleaseId (..)) +import Flora.Model.Release.Update qualified as Update + +insertTar + :: (Log :> es, DB :> es, BlobStoreAPI :> es, Time :> es) + => PackageName + -> Version + -> LazyByteString + -> Eff es (Either BlobStoreInsertError Sha256Sum) +insertTar pname version contents = do + mpackage <- Query.getPackageByNamespaceAndName (Namespace "hackage") pname + case mpackage of + Nothing -> pure . Left $ NoPackage pname + Just package -> do + mrelease <- Query.getReleaseByVersion (package.packageId) version + case mrelease of + Nothing -> pure . Left $ NoRelease pname version + Just release -> do + Update.updateTarballArchiveHash release.releaseId contents + case hashTree <$> tarballToTree pname version contents of + Left err -> pure . Left $ BlobStoreTarError pname version err + Right t@(TarRoot rootHash _ _ _) -> Right rootHash <$ insertTree (release.releaseId) t + +insertTree + :: (Log :> es, DB :> es, BlobStoreAPI :> es) + => ReleaseId + -> TarRoot Sha256Sum + -> Eff es () +insertTree releaseId t@(TarRoot rootHash _ _ tree) = do + Log.logTrace "Trying to insert directory tree" t + mTarballHash <- Query.getReleaseTarballRootHash releaseId + case mTarballHash of + Just tarballHash -> Log.logInfo_ $ "Hash already inserted with hash: " <> display tarballHash + Nothing -> do + Update.updateTarballRootHash releaseId rootHash + void $! M.traverseWithKey (insertBlobs rootHash) tree + Log.logInfo_ $ "Inserted hash tree with root " <> display rootHash + where + _onConflictDoNothing :: Query + _onConflictDoNothing = fromString "on conflict do nothing" + + insertDoNothing :: forall e m. (ToRow e, Entity e, MonadIO m) => e -> DBT m Int64 + insertDoNothing = execute Update (_insert @e <> _onConflictDoNothing) + + insertBlobs parentHash dir (TarDirectory childHash nodes) = do + res <- dbtToEff . insertDoNothing $! BlobRelation parentHash childHash dir True + when (res > 0) $! void $ M.traverseWithKey (insertBlobs childHash) nodes + void . dbtToEff . insertDoNothing $! BlobRelation parentHash childHash dir True + insertBlobs parentHash dir (TarFile childHash content) = do + put childHash content + void . dbtToEff . insertDoNothing $! BlobRelation parentHash childHash dir False diff --git a/src/core/Flora/Model/BlobStore/API.hs b/src/core/Flora/Model/BlobStore/API.hs new file mode 100644 index 00000000..2e6b7929 --- /dev/null +++ b/src/core/Flora/Model/BlobStore/API.hs @@ -0,0 +1,85 @@ +module Flora.Model.BlobStore.API + ( -- | Effect + BlobStoreAPI + , get + , put + , hashByteString + -- | Handlers + , runBlobStoreFS + , runBlobStorePure + ) +where + +import Crypto.Hash.SHA256 qualified as SHA +import Data.ByteString (ByteString) +import Data.ByteString.Char8 qualified as BS +import Data.Map.Strict qualified as M +import Data.Text qualified as T +import Data.Text.Display (display) +import System.Directory (createDirectoryIfMissing, doesFileExist) +import System.FilePath (()) + +import Effectful (Dispatch (..), DispatchOf, Eff, Effect, IOE, liftIO, type (:>)) +import Effectful.Dispatch.Dynamic (interpret, reinterpret, send) +import Effectful.State.Static.Local (evalState, gets, modify) + +import Flora.Model.BlobStore.Types + +data BlobStoreAPI :: Effect where + Get :: Sha256Sum -> BlobStoreAPI m (Maybe ByteString) + Put :: Sha256Sum -> ByteString -> BlobStoreAPI m () + +type instance DispatchOf BlobStoreAPI = Dynamic + +get :: BlobStoreAPI :> es => Sha256Sum -> Eff es (Maybe ByteString) +get = send . Get + +put :: BlobStoreAPI :> es => Sha256Sum -> ByteString -> Eff es () +put hash content = send (Put hash content) + +hashByteString :: ByteString -> Sha256Sum +hashByteString = Sha256Sum . SHA.hash + +-- | Run a blob store in a local filepath +runBlobStoreFS + :: forall es a + . IOE :> es + => FilePath + -> Eff (BlobStoreAPI : es) a + -> Eff es a +runBlobStoreFS fp e = do + liftIO $ createDirectoryIfMissing True fp + interpret (const handler) e + where + -- To avoid excessive entries in one directory we create directories of first + -- two characters + doesHashExist :: Sha256Sum -> Eff es (FilePath, Bool) + doesHashExist hash = liftIO $ do + let hashStr = T.unpack (display hash) + dir = fp take 2 hashStr + file = dir drop 2 hashStr + createDirectoryIfMissing True dir + exists <- doesFileExist file + pure (file, exists) + + -- Need to tie the handler type to the top level type + handler :: BlobStoreAPI (Eff localEs) a' -> Eff es a' + handler = \case + Get hash -> do + (file, exists) <- doesHashExist hash + if exists + then Just <$> liftIO (BS.readFile file) + else pure Nothing + Put hash content -> do + (file, exists) <- doesHashExist hash + if exists then pure () else liftIO $ BS.writeFile file content + +-- | Nun a pure in memory implementation of the blob store +-- +-- This should only really be used for testing +runBlobStorePure :: Eff (BlobStoreAPI : es) a -> Eff es a +runBlobStorePure = reinterpret (evalState $ M.empty @Sha256Sum @ByteString) $ + const $ + \case + Get hash -> gets $ M.lookup hash + Put hash content -> modify $ M.insert hash content diff --git a/src/core/Flora/Model/BlobStore/Types.hs b/src/core/Flora/Model/BlobStore/Types.hs new file mode 100644 index 00000000..45a9cc10 --- /dev/null +++ b/src/core/Flora/Model/BlobStore/Types.hs @@ -0,0 +1,38 @@ +module Flora.Model.BlobStore.Types where + +import Control.DeepSeq (NFData) +import Data.Aeson.Types +import Data.ByteString (ByteString) +import Data.Text (Text) +import Data.Text.Display (Display (..), display) +import Data.Text.Encoding (decodeUtf8Lenient, encodeUtf8) +import Data.Text.Lazy.Builder (fromText) +import GHC.Generics (Generic) + +import Data.ByteString.Base16 qualified as B16 +import Database.PostgreSQL.Simple.FromField (FromField (..)) +import Database.PostgreSQL.Simple.ToField (ToField (..)) + +newtype Sha256Sum = Sha256Sum {bytestring :: ByteString} + deriving (Eq, Show, Generic) + deriving newtype (Ord, NFData) + +instance ToField Sha256Sum where + toField = toField . decodeUtf8Lenient . B16.encode . bytestring + +instance FromField Sha256Sum where + fromField f mbs = + Sha256Sum . B16.decodeLenient . encodeUtf8 + <$> fromField @Text f mbs + +instance Display Sha256Sum where + displayBuilder = fromText . decodeUtf8Lenient . B16.encode . bytestring + +instance ToJSON Sha256Sum where + toJSON = String . display + +instance FromJSON Sha256Sum where + parseJSON (String txt) = pure . Sha256Sum . B16.decodeLenient $ encodeUtf8 txt + parseJSON invalid = + prependFailure "Parsing Sha256Sum failed" $! + typeMismatch "Invalid" invalid diff --git a/src/core/Flora/Model/Job.hs b/src/core/Flora/Model/Job.hs index 0636f9a5..a4c586f3 100644 --- a/src/core/Flora/Model/Job.hs +++ b/src/core/Flora/Model/Job.hs @@ -42,6 +42,14 @@ data ReadmeJobPayload = ReadmeJobPayload (ToJSON, FromJSON) via (CustomJSON '[FieldLabelModifier '[CamelToSnake]] ReadmeJobPayload) +data TarballJobPayload = TarballJobPayload + { package :: PackageName + , releaseId :: ReleaseId + , version :: IntAesonVersion + } + deriving stock (Generic) + deriving anyclass (ToJSON, FromJSON) + data UploadTimeJobPayload = UploadTimeJobPayload { packageName :: PackageName , releaseId :: ReleaseId @@ -71,6 +79,7 @@ data ImportHackageIndexPayload = ImportHackageIndexPayload -- these represent the possible odd jobs we can run. data FloraOddJobs = FetchReadme ReadmeJobPayload + | FetchTarball TarballJobPayload | FetchUploadTime UploadTimeJobPayload | FetchChangelog ChangelogJobPayload | ImportPackage ImportOutput diff --git a/src/core/Flora/Model/Package/Types.hs b/src/core/Flora/Model/Package/Types.hs index 8edacbbf..de3a039f 100644 --- a/src/core/Flora/Model/Package/Types.hs +++ b/src/core/Flora/Model/Package/Types.hs @@ -11,6 +11,7 @@ import Data.ByteString (ByteString) import Data.ByteString.Lazy (fromStrict) import Data.Maybe (fromJust, fromMaybe) import Data.OpenApi (Schema (..), ToParamSchema (..), ToSchema (..), genericDeclareNamedSchema) +import Data.String (IsString (..)) import Data.Text (Text, isPrefixOf, unpack) import Data.Text qualified as Text import Data.Text.Display @@ -85,6 +86,12 @@ parsePackageName txt = then Just $ PackageName txt else Nothing +instance IsString PackageName where + fromString = + fromMaybe (error "Bad package name") + . parsePackageName + . Text.pack + instance ToSchema PackageName where declareNamedSchema proxy = genericDeclareNamedSchema openApiSchemaOptions proxy diff --git a/src/core/Flora/Model/Release/Query.hs b/src/core/Flora/Model/Release/Query.hs index 2f0a8453..1f78778a 100644 --- a/src/core/Flora/Model/Release/Query.hs +++ b/src/core/Flora/Model/Release/Query.hs @@ -3,11 +3,13 @@ module Flora.Model.Release.Query ( getReleases - , getRelease + , getReleaseTarballRootHash + , getReleaseTarballArchive , getReleaseByVersion , getHackagePackageReleasesWithoutReadme , getHackagePackageReleasesWithoutChangelog , getHackagePackageReleasesWithoutUploadTimestamp + , getHackagePackageReleasesWithoutTarball , getAllReleases , getLatestReleaseTime , getNumberOfReleases @@ -17,6 +19,7 @@ module Flora.Model.Release.Query ) where +import Control.Monad (join) import Data.Text (Text) import Data.Time (UTCTime) import Data.Vector (Vector) @@ -31,7 +34,11 @@ import Distribution.Version (Version) import Effectful import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff) +import Data.ByteString (fromStrict) +import Data.ByteString.Lazy (LazyByteString) import Distribution.Orphans.Version () +import Flora.Model.BlobStore.API (BlobStoreAPI, get) +import Flora.Model.BlobStore.Types import Flora.Model.Component.Types import Flora.Model.Package.Types import Flora.Model.Release.Types @@ -54,6 +61,21 @@ getLatestReleaseTime repo = q = [sql| select max(r0.uploaded_at) from releases as r0 where r0.repository = ? |] q' = [sql| select max(uploaded_at) from releases |] +getReleaseTarballRootHash :: DB :> es => ReleaseId -> Eff es (Maybe Sha256Sum) +getReleaseTarballRootHash releaseId = dbtToEff $ do + mRelease <- selectOneByField @Release [field| release_id |] (Only releaseId) + case mRelease of + Just release -> pure $ tarballRootHash release + Nothing -> error $ "Internal error: searched for releaseId that doesn't exist: " <> show releaseId + +getReleaseTarballArchive :: (BlobStoreAPI :> es, DB :> es) => ReleaseId -> Eff es (Maybe LazyByteString) +getReleaseTarballArchive releaseId = do + mRelease <- dbtToEff $ selectOneByField @Release [field| release_id |] (Only releaseId) + case mRelease of + Nothing -> error $ "Internal error: searched for releaseId that doesn't exist: " <> show releaseId + Just release -> do + fmap fromStrict . join <$> traverse get release.tarballArchiveHash + getAllReleases :: DB :> es => PackageId -> Eff es (Vector Release) getAllReleases pid = dbtToEff $ do @@ -133,6 +155,21 @@ getHackagePackageReleasesWithoutChangelog = or p.namespace = 'haskell' |] +getHackagePackageReleasesWithoutTarball + :: DB :> es + => Eff es (Vector (ReleaseId, Version, PackageName)) +getHackagePackageReleasesWithoutTarball = + dbtToEff $! query Select querySpec () + where + querySpec = + [sql| + select r.release_id, r.version, p.name + from releases as r + join packages as p + on p.package_id = r.package_id + where r.tarball_root_hash is null + |] + getHackagePackagesWithoutReleaseDeprecationInformation :: DB :> es => Eff es (Vector (PackageName, Vector ReleaseId)) @@ -159,19 +196,6 @@ getReleaseByVersion packageId version = dbtToEff $ queryOne Select (_selectWhere @Release [[field| package_id |], [field| version |]]) (packageId, version) -getRelease - :: DB :> es - => Namespace - -> PackageName - -> Version - -> Eff es (Maybe Release) -getRelease namespace packageName version = - dbtToEff $ - queryOne - Select - (_selectWhere @Release [[field| namespace |], [field| package_name |], [field| version |]]) - (namespace, packageName, version) - getNumberOfReleases :: DB :> es => PackageId -> Eff es Word getNumberOfReleases pid = dbtToEff $ do diff --git a/src/core/Flora/Model/Release/Types.hs b/src/core/Flora/Model/Release/Types.hs index 67a01f96..09f27096 100644 --- a/src/core/Flora/Model/Release/Types.hs +++ b/src/core/Flora/Model/Release/Types.hs @@ -42,6 +42,7 @@ import Deriving.Aeson import Distribution.Orphans () import Distribution.Orphans.CompilerFlavor () import Distribution.Orphans.PackageFlag () +import Flora.Model.BlobStore.Types import Flora.Model.Package newtype ReleaseId = ReleaseId {getReleaseId :: UUID} @@ -88,6 +89,8 @@ data Release = Release , readmeStatus :: ImportStatus , changelog :: Maybe TextHtml , changelogStatus :: ImportStatus + , tarballRootHash :: Maybe Sha256Sum + , tarballArchiveHash :: Maybe Sha256Sum , license :: SPDX.License , sourceRepos :: Vector Text , homepage :: Maybe Text diff --git a/src/core/Flora/Model/Release/Update.hs b/src/core/Flora/Model/Release/Update.hs index d716c494..343dc033 100644 --- a/src/core/Flora/Model/Release/Update.hs +++ b/src/core/Flora/Model/Release/Update.hs @@ -4,6 +4,7 @@ module Flora.Model.Release.Update where import Control.Monad (void) +import Data.Text.Display (display) import Database.PostgreSQL.Entity import Database.PostgreSQL.Entity.DBT (QueryNature (Update), execute, executeMany) import Database.PostgreSQL.Entity.Types (field) @@ -12,10 +13,15 @@ import Database.PostgreSQL.Simple.SqlQQ (sql) import Effectful import Effectful.PostgreSQL.Transact.Effect +import Crypto.Hash.SHA256 qualified as SHA +import Data.ByteString (toStrict) +import Data.ByteString.Lazy (LazyByteString) import Data.Function ((&)) import Data.Time (UTCTime) import Data.Vector (Vector) import Data.Vector qualified as Vector +import Flora.Model.BlobStore.API (BlobStoreAPI, put) +import Flora.Model.BlobStore.Types import Flora.Model.Release.Types insertRelease :: DB :> es => Release -> Eff es () @@ -67,6 +73,30 @@ updateChangelog releaseId changelogBody status = ([field| release_id |], releaseId) (changelogBody, status) +updateTarballRootHash :: DB :> es => ReleaseId -> Sha256Sum -> Eff es () +updateTarballRootHash releaseId hash = + dbtToEff $ + void $ + updateFieldsBy @Release + [[field| tarball_root_hash |]] + ([field| release_id |], releaseId) + (Only $ Just $ display hash) + +updateTarballArchiveHash + :: (BlobStoreAPI :> es, DB :> es) + => ReleaseId + -> LazyByteString + -> Eff es () +updateTarballArchiveHash releaseId (toStrict -> content) = do + let hash = Sha256Sum . SHA.hash $ content + put hash content + dbtToEff $ + void $ + updateFieldsBy @Release + [[field| tarball_archive_hash |]] + ([field| release_id |], releaseId) + (Only . Just $ display hash) + setReleasesDeprecationMarker :: DB :> es => Vector (Bool, ReleaseId) -> Eff es () setReleasesDeprecationMarker releaseVersions = dbtToEff $ void $ executeMany Update q (releaseVersions & Vector.toList) @@ -76,5 +106,5 @@ setReleasesDeprecationMarker releaseVersions = UPDATE releases as r0 SET deprecated = upd.x FROM (VALUES (?,?)) as upd(x,y) - WHERE r0.release_id = (upd.y :: uuid) + WHERE r0.release_id = (upd.y :: uuid) |] diff --git a/src/datatypes/Database/PostgreSQL/Simple/Orphans.hs b/src/datatypes/Database/PostgreSQL/Simple/Orphans.hs new file mode 100644 index 00000000..2df5e805 --- /dev/null +++ b/src/datatypes/Database/PostgreSQL/Simple/Orphans.hs @@ -0,0 +1,9 @@ +{-# OPTIONS_GHC -Wno-orphans #-} + +module Database.PostgreSQL.Simple.Orphans where + +import Control.DeepSeq +import Data.ByteString (ByteString) +import Database.PostgreSQL.Simple.Types + +deriving newtype instance NFData (Binary ByteString) diff --git a/src/datatypes/Distribution/Orphans/Version.hs b/src/datatypes/Distribution/Orphans/Version.hs index f5577b57..7d470ae6 100644 --- a/src/datatypes/Distribution/Orphans/Version.hs +++ b/src/datatypes/Distribution/Orphans/Version.hs @@ -5,6 +5,8 @@ module Distribution.Orphans.Version where import Data.Aeson import Data.Aeson qualified as Aeson import Data.ByteString (ByteString) +import Data.Either (fromRight) +import Data.String (IsString (..)) import Data.Text qualified as Text import Data.Text.Display import Data.Text.Lazy.Builder qualified as Builder @@ -18,6 +20,9 @@ import Distribution.Types.Version qualified as Cabal import Distribution.Version (VersionRange) import Servant +instance IsString Version where + fromString = fromRight (error "Bad version") . eitherParsec + instance ToJSON Version where toJSON = Aeson.String . display . Pretty.prettyShow diff --git a/src/datatypes/Servant/API/ContentTypes/GZip.hs b/src/datatypes/Servant/API/ContentTypes/GZip.hs new file mode 100644 index 00000000..3ece22a4 --- /dev/null +++ b/src/datatypes/Servant/API/ContentTypes/GZip.hs @@ -0,0 +1,20 @@ +module Servant.API.ContentTypes.GZip where + +import Codec.Compression.GZip qualified as GZip +import Data.ByteString.Lazy (ByteString) +import Data.List.NonEmpty (NonEmpty (..)) +import Data.Typeable (Typeable) +import Network.HTTP.Media.MediaType ((//)) +import Servant.API.ContentTypes (Accept (..), MimeRender (..), MimeUnrender (..)) + +data GZipped + deriving (Typeable) + +instance Accept GZipped where + contentTypes _ = "application" // "x-gzip" :| [] + +instance MimeUnrender GZipped ByteString where + mimeUnrender _ = Right . GZip.decompress + +instance MimeRender GZipped ByteString where + mimeRender _ = GZip.compress diff --git a/src/jobs-worker/FloraJobs/Runner.hs b/src/jobs-worker/FloraJobs/Runner.hs index 68ae9bc5..ae74206f 100644 --- a/src/jobs-worker/FloraJobs/Runner.hs +++ b/src/jobs-worker/FloraJobs/Runner.hs @@ -9,6 +9,11 @@ import Data.Set qualified as Set import Data.Text.Display import Data.Vector (Vector) import Data.Vector qualified as Vector +import Effectful (Eff, IOE, type (:>)) +import Effectful.Log +import Effectful.PostgreSQL.Transact.Effect (DB) +import Effectful.Reader.Static (Reader) +import Effectful.Time (Time) import Log import Network.HTTP.Types (gone410, notFound404, statusCode) import OddJobs.Job (Job (..)) @@ -16,6 +21,8 @@ import Servant.Client (ClientError (..)) import Servant.Client.Core (ResponseF (..)) import Flora.Import.Package (coreLibraries, persistImportOutput, withWorkerDbPool) +import Flora.Model.BlobIndex.Update qualified as Update +import Flora.Model.BlobStore.API import Flora.Model.Job import Flora.Model.Package.Types import Flora.Model.Package.Update qualified as Update @@ -33,6 +40,7 @@ runner job = localDomain "job-runner" $ Error str -> logMessage LogAttention "decode error" (toJSON str) Success val -> case val of FetchReadme x -> makeReadme x + FetchTarball x -> fetchTarball x FetchUploadTime x -> fetchUploadTime x FetchChangelog x -> fetchChangeLog x ImportPackage x -> @@ -80,6 +88,45 @@ makeReadme pay@ReadmeJobPayload{..} = let readmeBody = renderMarkdown ("README" <> show mpPackage) bodyText Update.updateReadme mpReleaseId (Just $ MkTextHtml readmeBody) Imported +fetchTarball + :: ( IOE :> es + , Time :> es + , DB :> es + , Reader JobsRunnerEnv :> es + , Log :> es + , BlobStoreAPI :> es + ) + => TarballJobPayload + -> Eff es () +fetchTarball pay@TarballJobPayload{..} = do + localDomain "fetch-tarball" $ do + mArchive <- Query.getReleaseTarballArchive releaseId + content <- case mArchive of + Just bs -> pure bs + Nothing -> do + logInfo "Fetching tarball" pay + let payload = VersionedPackage{..} + result <- Hackage.request $ Hackage.getPackageTarball payload + case result of + Right bs -> pure bs + Left e@(FailureResponse _ response) -> do + logAttention "Could not fetch tarball from hackage" $ + object + [ "package" .= display payload.package + , "status_code" .= statusCode response.responseStatusCode + ] + throw e + Left e -> throw e + mhash <- Update.insertTar package (unIntAesonVersion version) content + case mhash of + Right hash -> + logInfo + ("Inserted tarball for " <> display package) + (object ["release_id" .= releaseId, "root_hash" .= hash]) + Left err -> do + logAttention_ $ "Failed to insert tarball for " <> display package + throw err + fetchUploadTime :: UploadTimeJobPayload -> JobsRunner () fetchUploadTime payload@UploadTimeJobPayload{packageName, packageVersion, releaseId} = localDomain "fetch-upload-time" $ do diff --git a/src/jobs-worker/FloraJobs/Scheduler.hs b/src/jobs-worker/FloraJobs/Scheduler.hs index bbd1f056..f14fb9a0 100644 --- a/src/jobs-worker/FloraJobs/Scheduler.hs +++ b/src/jobs-worker/FloraJobs/Scheduler.hs @@ -3,6 +3,7 @@ -- | Represents the various jobs that can be run module FloraJobs.Scheduler ( scheduleReadmeJob + , scheduleTarballJob , scheduleChangelogJob , scheduleUploadTimeJob , schedulePackageDeprecationListJob @@ -17,6 +18,7 @@ module FloraJobs.Scheduler ) where +import Data.Aeson (ToJSON) import Data.Pool import Data.Vector (Vector) import Database.PostgreSQL.Entity.DBT @@ -44,60 +46,35 @@ scheduleReadmeJob pool rid package version = (FetchReadme $ ReadmeJobPayload package rid $ MkIntAesonVersion version) ) +scheduleTarballJob :: Pool PG.Connection -> ReleaseId -> PackageName -> Version -> IO Job +scheduleTarballJob pool rid package version = + createJobWithResource pool $ FetchTarball $ TarballJobPayload package rid $ MkIntAesonVersion version + scheduleChangelogJob :: Pool PG.Connection -> ReleaseId -> PackageName -> Version -> IO Job scheduleChangelogJob pool rid package version = - withResource - pool - ( \res -> - createJob - res - jobTableName - (FetchChangelog $ ChangelogJobPayload package rid $ MkIntAesonVersion version) - ) + createJobWithResource pool $ FetchChangelog $ ChangelogJobPayload package rid $ MkIntAesonVersion version scheduleUploadTimeJob :: Pool PG.Connection -> ReleaseId -> PackageName -> Version -> IO Job scheduleUploadTimeJob pool releaseId packageName version = - withResource - pool - ( \res -> - createJob - res - jobTableName - (FetchUploadTime $ UploadTimeJobPayload packageName releaseId (MkIntAesonVersion version)) - ) + createJobWithResource pool $ + FetchUploadTime $ + UploadTimeJobPayload packageName releaseId (MkIntAesonVersion version) schedulePackageDeprecationListJob :: Pool PG.Connection -> IO Job schedulePackageDeprecationListJob pool = - withResource - pool - ( \conn -> - createJob - conn - jobTableName - FetchPackageDeprecationList - ) + createJobWithResource pool FetchPackageDeprecationList -scheduleReleaseDeprecationListJob :: Pool PG.Connection -> (PackageName, Vector ReleaseId) -> IO Job +scheduleReleaseDeprecationListJob + :: Pool PG.Connection -> (PackageName, Vector ReleaseId) -> IO Job scheduleReleaseDeprecationListJob pool (package, releaseIds) = - withResource - pool - ( \conn -> - createJob - conn - jobTableName - (FetchReleaseDeprecationList package releaseIds) - ) + createJobWithResource pool (FetchReleaseDeprecationList package releaseIds) scheduleRefreshLatestVersions :: Pool PG.Connection -> IO Job -scheduleRefreshLatestVersions pool = - withResource - pool - ( \conn -> - createJob - conn - jobTableName - RefreshLatestVersions - ) +scheduleRefreshLatestVersions pool = createJobWithResource pool RefreshLatestVersions + +createJobWithResource :: ToJSON p => Pool PG.Connection -> p -> IO Job +createJobWithResource pool job = + withResource pool $ \conn -> createJob conn jobTableName job checkIfIndexImportJobIsNotRunning :: JobsRunner Bool checkIfIndexImportJobIsNotRunning = do diff --git a/src/jobs-worker/FloraJobs/ThirdParties/Hackage/API.hs b/src/jobs-worker/FloraJobs/ThirdParties/Hackage/API.hs index 8aea200d..ebfc6caf 100644 --- a/src/jobs-worker/FloraJobs/ThirdParties/Hackage/API.hs +++ b/src/jobs-worker/FloraJobs/ThirdParties/Hackage/API.hs @@ -16,6 +16,7 @@ import Data.Vector (Vector) import Data.Vector qualified as Vector import Network.HTTP.Media ((//), (/:)) import Servant.API +import Servant.API.ContentTypes.GZip import Servant.API.Generic import Distribution.Orphans () @@ -44,6 +45,11 @@ instance ToHttpApiData VersionedPackage where toUrlPiece VersionedPackage{package, version} = display package <> "-" <> display version +newtype VersionedTarball = VersionedTarball VersionedPackage + +instance ToHttpApiData VersionedTarball where + toUrlPiece (VersionedTarball vt) = toUrlPiece vt <> ".tar.gz" + data HackageAPI' mode = HackageAPI' { listUsers :: mode :- "users" :> Get '[JSON] [HackageUserObject] , withUser :: mode :- "user" :> Capture "username" Text :> NamedRoutes HackageUserAPI @@ -65,6 +71,7 @@ data HackagePackageAPI mode = HackagePackageAPI , getDeprecatedReleases :: mode :- "preferred.json" :> Get '[JSON] HackagePreferredVersions , getPackageInfo :: mode :- Get '[JSON] HackagePackageInfo , getPackageWithRevision :: mode :- "revision" :> Capture "revision_number" Text :> Get '[JSON] HackagePackageInfo + , getTarball :: mode :- Capture "tarball" VersionedTarball :> Get '[GZipped] ByteString } deriving stock (Generic) diff --git a/src/jobs-worker/FloraJobs/ThirdParties/Hackage/Client.hs b/src/jobs-worker/FloraJobs/ThirdParties/Hackage/Client.hs index 7ba1e69f..fcc24cca 100644 --- a/src/jobs-worker/FloraJobs/ThirdParties/Hackage/Client.hs +++ b/src/jobs-worker/FloraJobs/ThirdParties/Hackage/Client.hs @@ -4,11 +4,13 @@ module FloraJobs.ThirdParties.Hackage.Client where import Control.Monad.IO.Class +import Data.ByteString.Lazy (ByteString) import Data.Proxy import Data.Text import Data.Time (UTCTime) import Data.Time.Orphans () import Data.Vector (Vector) +import Effectful (Eff, IOE, type (:>)) import Effectful.Reader.Static import Network.HTTP.Req (GET (GET), NoReqBody (..)) import Network.HTTP.Req qualified as Req @@ -17,9 +19,9 @@ import Servant.Client (BaseUrl (..), Client, ClientError (..), ClientM, Scheme ( import Flora.Model.Package.Types import FloraJobs.ThirdParties.Hackage.API as API -import FloraJobs.Types (JobsRunner, JobsRunnerEnv (..)) +import FloraJobs.Types (JobsRunnerEnv (..)) -request :: ClientM a -> JobsRunner (Either ClientError a) +request :: (IOE :> es, Reader JobsRunnerEnv :> es) => ClientM a -> Eff es (Either ClientError a) request req = do JobsRunnerEnv{httpManager} <- ask let clientEnv = @@ -37,6 +39,14 @@ listHackageUsers = hackageClient // API.listUsers getHackageUser :: Text -> ClientM HackageUserDetailsObject getHackageUser username = hackageClient // API.withUser /: username // API.getUser +getPackageTarball :: VersionedPackage -> ClientM ByteString +getPackageTarball versionedPackage = + hackageClient + // API.withPackage + /: versionedPackage + // API.getTarball + /: VersionedTarball versionedPackage + getPackageReadme :: VersionedPackage -> ClientM Text getPackageReadme versionedPackage = hackageClient diff --git a/src/jobs-worker/FloraJobs/Types.hs b/src/jobs-worker/FloraJobs/Types.hs index b1466179..92af877c 100644 --- a/src/jobs-worker/FloraJobs/Types.hs +++ b/src/jobs-worker/FloraJobs/Types.hs @@ -25,8 +25,10 @@ import OddJobs.ConfigBuilder import OddJobs.Job (Config (..), Job, LogEvent (..), LogLevel (..)) import OddJobs.Types (ConcurrencyControl (..), UIConfig (..)) +import Flora.Environment import Flora.Environment.Config import Flora.Logging qualified as Logging +import Flora.Model.BlobStore.API import Flora.Model.Job () type JobsRunner = @@ -34,18 +36,23 @@ type JobsRunner = '[ DB , Reader PoolConfig , Reader JobsRunnerEnv + , BlobStoreAPI , Log , Time , IOE ] -runJobRunner :: Pool Connection -> JobsRunnerEnv -> FloraConfig -> Logger -> JobsRunner a -> IO a -runJobRunner pool runnerEnv cfg logger jobRunner = +runJobRunner :: Pool Connection -> JobsRunnerEnv -> FloraEnv -> Logger -> JobsRunner a -> IO a +runJobRunner pool runnerEnv floraEnv logger jobRunner = runEff . runTime . LogEff.runLog "flora-jobs" logger defaultLogLevel + . ( case floraEnv.features.blobStoreImpl of + Just (BlobStoreFS fp) -> runBlobStoreFS fp + _ -> runBlobStorePure + ) . runReader runnerEnv - . runReader cfg.dbConfig + . runReader floraEnv.config.dbConfig . runDB pool $ jobRunner @@ -78,18 +85,18 @@ data JobsRunnerEnv = JobsRunnerEnv makeConfig :: JobsRunnerEnv - -> FloraConfig + -> FloraEnv -> Logger -> Pool PG.Connection -> (Job -> JobsRunner ()) -> Config -makeConfig runnerEnv cfg logger pool runnerContinuation = +makeConfig runnerEnv floraEnv logger pool runnerContinuation = mkConfig - (\level event -> structuredLogging cfg logger level event) + (\level event -> structuredLogging (floraEnv.config) logger level event) jobTableName pool (MaxConcurrentJobs 100) - (runJobRunner pool runnerEnv cfg logger . runnerContinuation) + (runJobRunner pool runnerEnv floraEnv logger . runnerContinuation) (\x -> x{cfgDeleteSuccessfulJobs = False, cfgDefaultMaxAttempts = 3}) makeUIConfig :: FloraConfig -> Logger -> Pool PG.Connection -> UIConfig diff --git a/src/web/FloraWeb/Common/Auth/Types.hs b/src/web/FloraWeb/Common/Auth/Types.hs index 79b839be..b2325653 100644 --- a/src/web/FloraWeb/Common/Auth/Types.hs +++ b/src/web/FloraWeb/Common/Auth/Types.hs @@ -16,6 +16,8 @@ import Servant.Server.Experimental.Auth (AuthServerData) import Web.Cookie (SetCookie) import Data.Text (Text) +import Flora.Environment +import Flora.Model.BlobStore.API import Flora.Model.PersistentSession import Flora.Model.User import FloraWeb.Types @@ -73,6 +75,8 @@ type FloraPage = , DB , Time , Reader (Headers '[Header "Set-Cookie" SetCookie] Session) + , Reader FeatureEnv + , BlobStoreAPI , Log , Error ServerError , IOE @@ -85,6 +89,8 @@ type FloraAdmin = , DB , Time , Reader (Headers '[Header "Set-Cookie" SetCookie] Session) + , Reader FeatureEnv + , BlobStoreAPI , Log , Error ServerError , IOE diff --git a/src/web/FloraWeb/Pages/Routes/Packages.hs b/src/web/FloraWeb/Pages/Routes/Packages.hs index 23b48d3a..bc57cdf7 100644 --- a/src/web/FloraWeb/Pages/Routes/Packages.hs +++ b/src/web/FloraWeb/Pages/Routes/Packages.hs @@ -4,11 +4,14 @@ module FloraWeb.Pages.Routes.Packages ) where +import Data.ByteString.Lazy (ByteString) import Data.Positive +import Data.Text (Text) import Distribution.Types.Version (Version) import Flora.Model.Package (Namespace, PackageName) import Lucid import Servant +import Servant.API.ContentTypes.GZip import Servant.API.Generic import Servant.HTML.Lucid @@ -82,5 +85,12 @@ data Routes' mode = Routes' :> Capture "package" PackageName :> "versions" :> Get '[HTML] (Html ()) + , getTarball + :: mode + :- Capture "namespace" Namespace + :> Capture "package" PackageName + :> Capture "version" Version + :> Capture "tarball" Text + :> Get '[GZipped] ByteString } deriving stock (Generic) diff --git a/src/web/FloraWeb/Pages/Server/Admin.hs b/src/web/FloraWeb/Pages/Server/Admin.hs index 70051d5c..c9f0c258 100644 --- a/src/web/FloraWeb/Pages/Server/Admin.hs +++ b/src/web/FloraWeb/Pages/Server/Admin.hs @@ -2,9 +2,13 @@ module FloraWeb.Pages.Server.Admin where import Control.Concurrent (forkIO) import Control.Concurrent.Async qualified as Async +import Control.Monad (void, when) import Control.Monad.IO.Class +import Data.Maybe (isJust) import Data.Proxy (Proxy (..)) import Database.PostgreSQL.Entity.DBT +import Effectful.Reader.Static (ask) +import Log qualified import Lucid import Network.HTTP.Types.Status (notFound404) import OddJobs.Endpoints qualified as OddJobs @@ -12,8 +16,7 @@ import OddJobs.Types qualified as OddJobs import Optics.Core import Servant (HasServer (..), hoistServer) -import Control.Monad (void) -import Flora.Environment (FloraEnv (..)) +import Flora.Environment (FeatureEnv (..), FloraEnv (..)) import Flora.Model.Admin.Report import Flora.Model.Package.Query qualified as Query import Flora.Model.Release.Query qualified as Query @@ -101,6 +104,19 @@ fetchMetadataHandler = do ( \(releaseId, version, packagename) -> scheduleChangelogJob jobsPool releaseId packagename version ) + features <- ask @FeatureEnv + Log.logAttention "features" features + when (isJust $ features.blobStoreImpl) $ do + releasesWithoutTarball <- Query.getHackagePackageReleasesWithoutTarball + liftIO $! + void $! + forkIO $! + Async.forConcurrently_ + releasesWithoutTarball + ( \(releaseId, version, packagename) -> + scheduleTarballJob jobsPool releaseId packagename version + ) + packagesWithoutDeprecationInformation <- Query.getHackagePackagesWithoutReleaseDeprecationInformation liftIO $ void $ diff --git a/src/web/FloraWeb/Pages/Server/Packages.hs b/src/web/FloraWeb/Pages/Server/Packages.hs index cc83e5c8..98adcf73 100644 --- a/src/web/FloraWeb/Pages/Server/Packages.hs +++ b/src/web/FloraWeb/Pages/Server/Packages.hs @@ -4,22 +4,30 @@ module FloraWeb.Pages.Server.Packages ) where +import Control.Monad (unless) +import Data.ByteString.Lazy (ByteString) import Data.Foldable import Data.Function import Data.Map.Strict as Map -import Data.Maybe (fromMaybe, isNothing) +import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Positive +import Data.Text (Text) import Data.Text.Display (display) import Data.Vector qualified as Vector import Distribution.Orphans () import Distribution.Types.Version (Version) +import Effectful.Error.Static (throwError) +import Effectful.Reader.Static (ask) import Log (object, (.=)) import Log qualified import Lucid import Lucid.Orphans () import Servant (ServerT) +import Servant.Server (err404) +import Flora.Environment (FeatureEnv (..)) import Flora.Logging +import Flora.Model.BlobIndex.Query qualified as Query import Flora.Model.Package import Flora.Model.Package.Query qualified as Query import Flora.Model.Release.Query qualified as Query @@ -50,6 +58,7 @@ server = , showChangelog = showChangelogHandler , showVersionChangelog = showVersionChangelogHandler , listVersions = listVersionsHandler + , getTarball = getTarballHandler } listPackagesHandler :: Maybe (Positive Word) -> FloraPage (Html ()) @@ -231,3 +240,18 @@ listVersionsHandler namespace packageName = do } releases <- Query.getAllReleases package.packageId render templateEnv $ Package.listVersions namespace packageName releases + +constructTarballPath :: PackageName -> Version -> Text +constructTarballPath pname v = display pname <> "-" <> display v <> ".tar.gz" + +getTarballHandler :: Namespace -> PackageName -> Version -> Text -> FloraPage ByteString +getTarballHandler namespace packageName version tarballName = do + features <- ask @FeatureEnv + unless (isJust $ features.blobStoreImpl) $! throwError err404 + package <- guardThatPackageExists namespace packageName $ \_ _ -> web404 + release <- guardThatReleaseExists package.packageId version $ const web404 + case release.tarballRootHash of + Just rootHash + | constructTarballPath packageName version == tarballName -> + Query.queryTar packageName version rootHash + _ -> throwError err404 diff --git a/src/web/FloraWeb/Pages/Templates/Error.hs b/src/web/FloraWeb/Pages/Templates/Error.hs index 92af293e..8a4d9154 100644 --- a/src/web/FloraWeb/Pages/Templates/Error.hs +++ b/src/web/FloraWeb/Pages/Templates/Error.hs @@ -13,6 +13,7 @@ import Data.Kind (Type) import Effectful import Effectful.Error.Static (Error, throwError) import Effectful.Reader.Static (Reader) +import Flora.Environment (FeatureEnv) import FloraWeb.Pages.Templates import FloraWeb.Session import Servant (Header, Headers, ServerError (..)) @@ -39,6 +40,7 @@ web404 :: ( Error ServerError :> es , IOE :> es , Reader (Headers '[Header "Set-Cookie" SetCookie] Session) :> es + , Reader FeatureEnv :> es ) => Eff es a web404 = do diff --git a/src/web/FloraWeb/Pages/Templates/Packages.hs b/src/web/FloraWeb/Pages/Templates/Packages.hs index 1ae8a44b..7858005d 100644 --- a/src/web/FloraWeb/Pages/Templates/Packages.hs +++ b/src/web/FloraWeb/Pages/Templates/Packages.hs @@ -3,6 +3,7 @@ module FloraWeb.Pages.Templates.Packages where import Control.Monad (when) +import Control.Monad.Reader (ask) import Data.List qualified as List import Data.Map.Strict qualified as Map import Data.Positive @@ -27,8 +28,9 @@ import Text.PrettyPrint (Doc, hcat, render) import Text.PrettyPrint qualified as PP import Data.Foldable (fold) -import Data.Maybe (fromJust) +import Data.Maybe (fromJust, isJust) import Distribution.Pretty (pretty) +import Flora.Environment (FeatureEnv (..)) import Flora.Model.Category.Types import Flora.Model.Package import Flora.Model.Release.Types @@ -38,7 +40,7 @@ import FloraWeb.Components.PackageListItem (licenseIcon, packageListItem, requir import FloraWeb.Components.PaginationNav (paginationNav) import FloraWeb.Components.Utils (text) import FloraWeb.Links qualified as Links -import FloraWeb.Pages.Templates (FloraHTML) +import FloraWeb.Pages.Templates (FloraHTML, TemplateEnv (..)) import FloraWeb.Pages.Templates.Haddock (renderHaddock) data Target @@ -302,8 +304,8 @@ showAll target mVersion namespace packageName = do Versions -> Links.packageVersions namespace packageName a_ [class_ "dependency", href_ ("/" <> toUrlPiece resource)] "Show all…" -displayInstructions :: PackageName -> Release -> FloraHTML -displayInstructions packageName latestRelease = +displayInstructions :: Namespace -> PackageName -> Release -> FloraHTML +displayInstructions namespace packageName latestRelease = li_ [class_ ""] $ do h3_ [class_ "package-body-section"] "Installation" div_ [class_ "items-top"] $ div_ [class_ ""] $ do @@ -315,6 +317,13 @@ displayInstructions packageName latestRelease = , value_ (formatInstallString packageName latestRelease) , readonly_ "readonly" ] + TemplateEnv{features} <- ask + when (isJust $ features.blobStoreImpl) $ do + label_ [for_ "tarball", class_ "font-light"] "Download" + let v = display latestRelease.version + tarballName = display packageName <> "-" <> v <> ".tar.gz" + tarballLink = "/packages/" <> display namespace <> "/" <> display packageName <> "/" <> v <> "/" <> tarballName + div_ $ a_ [href_ tarballLink, download_ ""] $ toHtml tarballName displayPackageDeprecation :: PackageAlternatives -> FloraHTML displayPackageDeprecation (PackageAlternatives inFavourOf) = diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs b/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs index c6faf00b..2225bf55 100644 --- a/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs +++ b/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs @@ -116,7 +116,7 @@ packageBody Nothing -> if fromMaybe False deprecated then displayReleaseDeprecation (getLatestViableRelease namespace packageName packageReleases) - else displayInstructions packageName latestRelease + else displayInstructions namespace packageName latestRelease displayTestedWith latestRelease.testedWith displayDependencies (namespace, packageName, version) numberOfDependencies dependencies displayDependents (namespace, packageName) numberOfDependents dependents diff --git a/src/web/FloraWeb/Pages/Templates/Types.hs b/src/web/FloraWeb/Pages/Templates/Types.hs index 81ad4cb1..9168df6f 100644 --- a/src/web/FloraWeb/Pages/Templates/Types.hs +++ b/src/web/FloraWeb/Pages/Templates/Types.hs @@ -13,13 +13,15 @@ module FloraWeb.Pages.Templates.Types where import Control.Monad.Identity -import Control.Monad.Reader +import Control.Monad.Reader (ReaderT) import Data.Text (Text) import Data.UUID qualified as UUID import GHC.Generics import Lucid import Optics.Core +import Effectful +import Effectful.Reader.Static (Reader, ask) import Flora.Environment import Flora.Environment.Config (Assets) import Flora.Model.PersistentSession (PersistentSessionId (..)) @@ -51,6 +53,7 @@ data TemplateEnv = TemplateEnv , mUser :: Maybe User , sessionId :: PersistentSessionId , environment :: DeploymentEnv + , features :: FeatureEnv , activeElements :: ActiveElements , assets :: Assets , indexPage :: Bool @@ -75,6 +78,7 @@ data TemplateDefaults = TemplateDefaults , description :: Text , mUser :: Maybe User , environment :: DeploymentEnv + , features :: FeatureEnv , activeElements :: ActiveElements , indexPage :: Bool } @@ -100,6 +104,7 @@ defaultTemplateEnv = , description = "Package index for the Haskell ecosystem" , mUser = Nothing , environment = Development + , features = FeatureEnv Nothing , activeElements = defaultActiveElements , indexPage = True } @@ -111,18 +116,20 @@ defaultsToEnv assets TemplateDefaults{..} = in TemplateEnv{..} fromSession - :: MonadIO m + :: (Reader FeatureEnv :> es, IOE :> es) => Session -> TemplateDefaults - -> m TemplateEnv + -> Eff es TemplateEnv fromSession session defaults = do let sessionId = session.sessionId let muser = session.mUser let webEnvStore = session.webEnvStore floraEnv <- liftIO $ fetchFloraEnv webEnvStore + featuresEnv <- ask @FeatureEnv let assets = floraEnv.assets let TemplateDefaults{..} = defaults & (#mUser .~ muser) & (#environment .~ (floraEnv.environment)) + & (#features .~ featuresEnv) pure TemplateEnv{..} diff --git a/src/web/FloraWeb/Server.hs b/src/web/FloraWeb/Server.hs index e774ba1e..8241ad5b 100644 --- a/src/web/FloraWeb/Server.hs +++ b/src/web/FloraWeb/Server.hs @@ -59,10 +59,11 @@ import Servant.API (getResponse) import Servant.OpenApi import Servant.Server.Generic (AsServerT, genericServeTWithContext) -import Flora.Environment (DeploymentEnv, FloraEnv (..), LoggingEnv (..), getFloraEnv) +import Flora.Environment (BlobStoreImpl (..), DeploymentEnv, FeatureEnv (..), FloraEnv (..), LoggingEnv (..), getFloraEnv) import Flora.Environment.Config (Assets) import Flora.Logging (runLog) import Flora.Logging qualified as Logging +import Flora.Model.BlobStore.API import FloraJobs.Runner (runner) import FloraJobs.Types (JobsRunnerEnv (..), makeConfig, makeUIConfig) import FloraWeb.API.Routes qualified as API @@ -125,7 +126,7 @@ runServer appLogger floraEnv = do oddJobsCfg = makeConfig runnerEnv - (floraEnv.config) + floraEnv appLogger (floraEnv.jobsPool) runner @@ -162,7 +163,7 @@ mkServer -> Application mkServer logger webEnvStore floraEnv cfg jobsRunnerEnv = do genericServeTWithContext - (naturalTransform (floraEnv.environment) logger webEnvStore) + (naturalTransform (floraEnv.environment) (floraEnv.features) logger webEnvStore) (floraServer (floraEnv.pool) cfg jobsRunnerEnv) (genAuthServerContext logger floraEnv) @@ -216,10 +217,15 @@ floraServer pool cfg jobsRunnerEnv = , docs = serveDirectoryWith docsBundler } -naturalTransform :: DeploymentEnv -> Logger -> WebEnvStore -> Flora a -> Handler a -naturalTransform deploymentEnv logger webEnvStore app = +naturalTransform :: DeploymentEnv -> FeatureEnv -> Logger -> WebEnvStore -> Flora a -> Handler a +naturalTransform deploymentEnv features logger webEnvStore app = app & runReader webEnvStore + & runReader features + & ( case features.blobStoreImpl of + Just (BlobStoreFS fp) -> runBlobStoreFS fp + _ -> runBlobStorePure + ) & runLog deploymentEnv logger & effToHandler diff --git a/src/web/FloraWeb/Types.hs b/src/web/FloraWeb/Types.hs index 82a1814d..6a7b06e1 100644 --- a/src/web/FloraWeb/Types.hs +++ b/src/web/FloraWeb/Types.hs @@ -30,11 +30,14 @@ import Servant (FromHttpApiData (..), Handler, ServerError) import Web.Cookie import Flora.Environment +import Flora.Model.BlobStore.API type Flora :: Type -> Type type Flora = Eff '[ Reader WebEnvStore + , Reader FeatureEnv + , BlobStoreAPI , Log , Error ServerError , IOE @@ -45,6 +48,8 @@ type FloraAPI = '[ DB , Time , Reader () + , Reader FeatureEnv + , BlobStoreAPI , Log , Error ServerError , IOE diff --git a/test/Flora/BlobSpec.hs b/test/Flora/BlobSpec.hs new file mode 100644 index 00000000..6680fa9b --- /dev/null +++ b/test/Flora/BlobSpec.hs @@ -0,0 +1,100 @@ +module Flora.BlobSpec where + +import Codec.Archive.Tar qualified as Tar +import Codec.Archive.Tar.Entry qualified as Tar +import Codec.Compression.GZip qualified as GZip +import Control.Arrow +import Control.Monad.IO.Class +import Data.ByteString.Lazy (LazyByteString) +import Data.ByteString.Lazy qualified as BL +import Data.Function (on) +import Data.List (sortBy) +import Distribution.Version (mkVersion) +import System.FilePath (()) + +import Data.Foldable (traverse_) +import Data.Maybe (fromJust) +import Flora.Model.BlobIndex.Query qualified as Query +import Flora.Model.BlobIndex.Types +import Flora.Model.BlobIndex.Update qualified as Update +import Flora.Model.Package +import Flora.Model.Package.Query qualified as Query +import Flora.Model.Package.Types () +import Flora.Model.Release.Query qualified as Query +import Flora.Model.Release.Types +import Flora.TestUtils + +spec :: TestEff TestTree +spec = + testThese + "Blob store tests" + [ testThis "Import tarball" testImportTarball + , testThis "Import bad tarball" testBadTarball + , testThis "Import malformed tarball" testMalformedTarball + ] + +-- Util function to extract a list from Tar.Entries which is easier to compare +toList :: Tar.Entries e -> Either e [Tar.Entry] +toList = right reverse . left fst . Tar.foldlEntries (\acc x -> x : acc) [] + +readTarball :: FilePath -> TestEff LazyByteString +readTarball tarball = liftIO $ GZip.decompress <$> BL.readFile ("test/fixtures/tarballs" tarball) + +testImportTarball :: TestEff () +testImportTarball = do + content <- readTarball "b-0.1.0.0.tar.gz" + let pname = PackageName "b" + version = mkVersion [0, 1, 0, 0] + res <- Update.insertTar pname version content + case res of + Left err -> assertFailure (show err) + Right hash -> do + content' <- Query.queryTar pname version hash + case toList . Tar.read <$> [content, content'] of + [Right tarEntries, Right tarEntries'] -> do + -- check we've not lost or gained any entries + assertEqual (length tarEntries) (length tarEntries') + -- Check the output order is sorted + checkAll Tar.entryPath (sortByPath tarEntries') tarEntries' + -- Check both paths and content are the same in input and output + checkAll Tar.entryPath (sortByPath tarEntries) tarEntries' + checkAll Tar.entryContent (sortByPath tarEntries) tarEntries' + + -- check that we also archived the initial tarball along with the release + package <- fromJust <$> Query.getPackageByNamespaceAndName (Namespace "hackage") pname + release <- fromJust <$> Query.getReleaseByVersion package.packageId version + archivedContent <- fromJust <$> Query.getReleaseTarballArchive release.releaseId + assertEqual content archivedContent + [Left _, _] -> assertFailure "Input tar is corrupted" + [_, Left _] -> assertFailure "Generated corrupted tarball" + _ -> assertFailure "Something impossible happened!" + where + sortByPath = sortBy (compare `on` Tar.entryPath) + -- traverse the two lists asserting equality of the results of a + -- function on each element + checkAll f xs ys = + traverse_ (uncurry assertEqual . (f *** f)) $ + zip xs ys + +testBadTarball :: TestEff () +testBadTarball = do + content <- readTarball "bad-tar-0.1.0.0.tar.gz" + let pname = PackageName "bad-tar" + version = mkVersion [0, 1, 0, 0] + res <- Update.insertTar pname version content + case res of + Right _ -> assertFailure "Imported bad tarball" + Left (BlobStoreTarError _ _ (TarUnsupportedEntry entry)) -> + assertEqual entry (Tar.SymbolicLink $ fromJust $ Tar.toLinkTarget "src/Lib.hs") + Left err -> assertFailure $ "Unexpected error " <> show err + +testMalformedTarball :: TestEff () +testMalformedTarball = do + content <- readTarball "malformed-tar-0.1.0.0.tar.gz" + let pname = PackageName "malformed-tar" + version = mkVersion [0, 1, 0, 0] + res <- Update.insertTar pname version content + case res of + Right _ -> assertFailure "Imported malformed tarball" + Left (BlobStoreTarError _ _ (TarUnexpectedLayout path)) -> assertEqual path "b-0.1.0.0" + Left _ -> assertFailure "Unexpected error" diff --git a/test/Flora/ImportSpec.hs b/test/Flora/ImportSpec.hs index cc31e671..a25bea82 100644 --- a/test/Flora/ImportSpec.hs +++ b/test/Flora/ImportSpec.hs @@ -27,7 +27,7 @@ spec fixtures = ] testIndex :: FilePath -testIndex = "./test/fixtures/test-index.tar.gz" +testIndex = "./test/fixtures/tarballs/test-index.tar.gz" defaultRepo :: Text defaultRepo = "test-namespace" diff --git a/test/Flora/PackageSpec.hs b/test/Flora/PackageSpec.hs index 6e63458f..649d5254 100644 --- a/test/Flora/PackageSpec.hs +++ b/test/Flora/PackageSpec.hs @@ -225,7 +225,7 @@ testGetNonDeprecatedPackages = do testReleaseDeprecation :: TestEff () testReleaseDeprecation = do result <- Query.getHackagePackagesWithoutReleaseDeprecationInformation - assertEqual 66 (length result) + assertEqual 68 (length result) binary <- fromJust <$> Query.getPackageByNamespaceAndName (Namespace "haskell") (PackageName "binary") deprecatedBinaryVersion' <- assertJust =<< Query.getReleaseByVersion (binary.packageId) (mkVersion [0, 10, 0, 0]) diff --git a/test/Flora/TestUtils.hs b/test/Flora/TestUtils.hs index d75630b8..96f802fb 100644 --- a/test/Flora/TestUtils.hs +++ b/test/Flora/TestUtils.hs @@ -100,6 +100,7 @@ import Flora.Environment.Config (LoggingDestination (..), PoolConfig (..)) import Flora.Import.Categories (importCategories) import Flora.Import.Package.Bulk (importAllFilesInRelativeDirectory, importFromIndex) import Flora.Logging qualified as Logging +import Flora.Model.BlobStore.API import Flora.Model.User import Flora.Model.User.Query qualified as Query import Flora.Model.User.Update @@ -107,7 +108,7 @@ import Flora.Model.User.Update qualified as Update import Flora.Publish import FloraWeb.Client -type TestEff = Eff '[Fail, Reader PoolConfig, DB, Log, Time, IOE] +type TestEff = Eff '[Fail, BlobStoreAPI, Reader PoolConfig, DB, Log, Time, IOE] data Fixtures = Fixtures { hackageUser :: User @@ -136,6 +137,7 @@ runTestEff comp pool poolCfg = runEff $ . Log.runLog "flora-test" stdOutLogger LogAttention . runDB pool . runReader poolCfg + . runBlobStorePure . runFailIO $ comp diff --git a/test/Main.hs b/test/Main.hs index ce37df39..37e1e4c7 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -11,6 +11,9 @@ import Log.Data import System.IO import Test.Tasty (defaultMain, testGroup) +import Flora.Model.BlobStore.API + +import Flora.BlobSpec qualified as BlobSpec import Flora.CabalSpec qualified as CabalSpec import Flora.CategorySpec qualified as CategorySpec import Flora.Environment @@ -31,6 +34,7 @@ main = do . Log.runLog "flora-test" stdOutLogger LogInfo . runDB env.pool . runReader env.dbConfig + . runBlobStorePure . runFailIO $ do Update.createPackageIndex "hackage" "" Nothing @@ -49,4 +53,5 @@ specs fixtures = , TemplateSpec.spec , CabalSpec.spec , ImportSpec.spec fixtures + , BlobSpec.spec ] diff --git a/test/fixtures/Cabal/bad-tar.cabal b/test/fixtures/Cabal/bad-tar.cabal new file mode 100644 index 00000000..92559d38 --- /dev/null +++ b/test/fixtures/Cabal/bad-tar.cabal @@ -0,0 +1,17 @@ +cabal-version: 3.0 +-- Initial a.cabal generated by cabal init. For further documentation, see +-- http://haskell.org/cabal/users-guide/ + +name: bad-tar +version: 0.1.0.0 +-- synopsis: +-- description: +-- license: +author: Raoul Hidalgo Charman +build-type: Simple +extra-source-files: ChangeLog.md + +executable e + Main-is: A.hs + default-language: Haskell2010 + build-depends: base >=4 && <5 diff --git a/test/fixtures/Cabal/malformed-tar.cabal b/test/fixtures/Cabal/malformed-tar.cabal new file mode 100644 index 00000000..5f3c4e31 --- /dev/null +++ b/test/fixtures/Cabal/malformed-tar.cabal @@ -0,0 +1,17 @@ +cabal-version: 3.0 +-- Initial a.cabal generated by cabal init. For further documentation, see +-- http://haskell.org/cabal/users-guide/ + +name: malformed-tar +version: 0.1.0.0 +-- synopsis: +-- description: +-- license: +author: Raoul Hidalgo Charman +build-type: Simple +extra-source-files: ChangeLog.md + +executable e + Main-is: A.hs + default-language: Haskell2010 + build-depends: base >=4 && <5 diff --git a/test/fixtures/Tarball/tar-a/0.1.0.0/tar-a.cabal b/test/fixtures/Tarball/tar-a/0.1.0.0/tar-a.cabal deleted file mode 100644 index d1eb0333..00000000 --- a/test/fixtures/Tarball/tar-a/0.1.0.0/tar-a.cabal +++ /dev/null @@ -1,35 +0,0 @@ -cabal-version: 3.0 --- Initial a.cabal generated by cabal init. For further documentation, see --- http://haskell.org/cabal/users-guide/ - -name: tar-a -version: 0.1.0.0 --- synopsis: --- description: --- license: -author: Francesco Gazzetta ---maintainer: --- copyright: --- category: -build-type: Simple -extra-source-files: ChangeLog.md - -executable e - Main-is: A.hs - default-language: Haskell2010 - build-depends: base >=4 && <5 - , tar-b:{ sublib - , anothersublib - -- You can include sublibraries from hackage, like - --, cabal-plan:{topograph} - } - -library - exposed-modules: A1 - -- other-modules: - -- other-extensions: - build-depends: base >=4 && <5 - , tar-b:{ sublib - , anothersublib } - -- hs-source-dirs: - default-language: Haskell2010 diff --git a/test/fixtures/Tarball/tar-b/0.1.0.0/tar-b.cabal b/test/fixtures/Tarball/tar-b/0.1.0.0/tar-b.cabal deleted file mode 100644 index 484f4da5..00000000 --- a/test/fixtures/Tarball/tar-b/0.1.0.0/tar-b.cabal +++ /dev/null @@ -1,40 +0,0 @@ -cabal-version: 3.0 --- Initial b.cabal generated by cabal init. For further documentation, see --- http://haskell.org/cabal/users-guide/ - -name: tar-b -version: 0.1.0.0 --- synopsis: --- description: --- license: -author: Francesco Gazzetta ---maintainer: --- copyright: --- category: -build-type: Simple -extra-source-files: ChangeLog.md - -library - exposed-modules: BTop - -- other-modules: - -- other-extensions: - build-depends: base >=4 && <5 - --, sublib - -- hs-source-dirs: - default-language: Haskell2010 - -library sublib - visibility: public - exposed-modules: BSub - -- other-modules: - -- other-extensions: - build-depends: base >=4 && <5 - -- hs-source-dirs: - default-language: Haskell2010 -library anothersublib - visibility: public - -- other-modules: - -- other-extensions: - build-depends: base >=4 && <5 - -- hs-source-dirs: - default-language: Haskell2010 diff --git a/test/fixtures/tarballs/b-0.1.0.0.tar.gz b/test/fixtures/tarballs/b-0.1.0.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..847e82ed472065d6b12d9d36af54312d4049f594 GIT binary patch literal 695 zcmV;o0!aNIiwFolOfO{s17a;OE-@}JE-)^1VR8WNnBQ*OAQZ-P?Wf?XSF;20pGeWR zi%se_X?NWVfH}3%h_L{7b)SA1H)$G0ow7RCrt-V7AmnfkpZy&tEXD~*aDtbMi-8^M zJRh!Pont>HS>~^gEJ&6Uk`+Z>xOuX2qXo(@m(mlRu~Z|p(6!)HR;sM2KD`dKnIoZj zVSD}A#qNM}^j~Fo(7z~x{aMqHKM z78*;fi}(&1fFTyDt!+2UWlhabkTOxaTKW)6f3R`Y2@cCBYUmbrY3yOqQGz3nckU40 z^o?qbFq>h(!IWCG!~T9OMG1|8O+-6eD}5CHTGOU<-%#iaeRu$Csr$~B3Ri(}_4){< zYI`lJ+KwZ429?tNCSskCJhpv%nDdR;wh|(^x0=RAb-IN3R!A^z&F7jn6?|7!vgJ`E z1=Cdb5khd^Dg!*;D&F~(pMI#;&AZ*yaH{(c4(Hqst}a)lpB{7J&_ct_E*(Y2Q-NgGR-d`GLX*sr6KD@^ddlBYO#!U!g$ zu>A(L!(@4~%A1Z|qRPLkVpjxh{LU}e@{i>F+UNh!|BWs$@(yrT|0l%xf0|`Q=>KzY ztpDIUNj+_$f9U@huU1F;XN7wW4E=uwM)&gV2K{o@1+R9L!2iUv=ifKH>07{A{=Z%i z&p*l2;Qw=QLjU2VY|<8Rmj8SG^EAzZ|IfkB|DEQ+|KA{o&;PUEJrlNp^Z)O|ED^vd_ zEHUw%|4ei+~&48rANQbHgj#2#@*!r>ez$#C4VzzM-IPH>c_3%HmOyg+2yG>+42 zC>)`Ms(DF8C5xJ>x3M$l3%Fj@AFdsj|k2(0^>MJ zi1Ythc+x))3Yt@KfjLh2KhC!L<0MTnw*DWdN#y*04hGHrJE)Dzc7;abAoP46eQCMj zRG{sXfhyKAMGY%azCq&f4vHMPH_y`-@G2Hp# ztL$JLW|MYRA1~~W!37he-shd|L5Sj{?>IS+Vt&ldjA(4=$|Aw zbNzn~w);OTyxRc2LiXPOq1|1g1)SyoS!Vq|izDa%bMTY?_LFSl7I2pT5Bd|k4RroL z13Ui*)t&#pLZ06LXS<$B+rU}=pM>`Pk3;wWpTpsBI2;a#!!bEO0Cj5$ZvZF&06Bh+ AQvd(} literal 0 HcmV?d00001 diff --git a/test/fixtures/test-index.tar.gz b/test/fixtures/tarballs/test-index.tar.gz similarity index 100% rename from test/fixtures/test-index.tar.gz rename to test/fixtures/tarballs/test-index.tar.gz From fdb77cbc4fc9fa0a18d666cad13cd57e84bfc2a2 Mon Sep 17 00:00:00 2001 From: Raoul Hidalgo Charman Date: Thu, 26 Oct 2023 00:34:43 +0700 Subject: [PATCH 19/40] [FLORA-351] Display components in dependency page (#464) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Théophile Choutri closes #351 --- CHANGELOG.md | 3 ++- assets/css/1-core/2-variables.css | 1 + assets/css/styles.css | 7 ++++- ...31020123433_add_requirement_components.sql | 4 +++ src/core/Flora/Import/Package.hs | 17 ++++++------ src/core/Flora/Model/Package/Query.hs | 6 +++-- src/core/Flora/Model/Package/Update.hs | 2 +- src/core/Flora/Model/Requirement.hs | 26 ++++--------------- .../FloraWeb/Components/PackageListItem.hs | 20 +++++++++++--- test/fixtures/Cabal/b.cabal | 2 +- 10 files changed, 49 insertions(+), 39 deletions(-) create mode 100644 migrations/20231020123433_add_requirement_components.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 533e9d63..557898b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,12 @@ # CHANGELOG ## 1.0.14 -- XXXX-XX-XX -* Added more matches to the the natural language processing catergory ([#44](/~https://github.com/flora-pm/flora-server/pull/440)) * Colourise in red deprecation markers on the package page ([#438](/~https://github.com/flora-pm/flora-server/pull/439)) +* Added more matches to the the natural language processing catergory ([#44](/~https://github.com/flora-pm/flora-server/pull/440)) * Allow package imports from multiple repositories ([#444](/~https://github.com/flora-pm/flora-server/pull/444)) * Add a page on namespaces in the documentation ([#451](/~https://github.com/flora-pm/flora-server/pull/451)) * Add initial support for hosting package tarballs ([#452](/~https://github.com/flora-pm/flora-server/pull/452)) +* Show depended on components in dependencies page ([#464](/~https://github.com/flora-pm/flora-server/pull/464)) ## 1.0.13 -- 2023-09-17 * Exclude deprecated releases from latest versions and search ([#373](/~https://github.com/flora-pm/flora-server/pull/373)) diff --git a/assets/css/1-core/2-variables.css b/assets/css/1-core/2-variables.css index 8bba517e..d6e04f4b 100644 --- a/assets/css/1-core/2-variables.css +++ b/assets/css/1-core/2-variables.css @@ -50,6 +50,7 @@ --package-list-item-synopsis-color: black; --package-list-item-metadata-color: black; --package-list-item-version-color: var(--green-30); + --package-list-item-component-color: var(--gray-80); --search-bar-color: hsl(221 39% 11%); --search-bar-background-color: var(--gray-100); --search-bar-background-hover-color: white; diff --git a/assets/css/styles.css b/assets/css/styles.css index 7825d9f8..3932e3b5 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -100,12 +100,12 @@ .package-list-item__name { display: inline; - margin-right: 0.5rem; color: var(--package-list-item-name-color); } .package-list-item__synopsis { display: inline; + margin-left: 10px; color: var(--package-list-item-synopsis-color); } @@ -127,6 +127,11 @@ font-size: 0.875rem; line-height: 1.25rem; } + + .package-list-item__component { + display: inline; + color: var(--package-list-item-component-color); + } } .category a:hover { diff --git a/migrations/20231020123433_add_requirement_components.sql b/migrations/20231020123433_add_requirement_components.sql new file mode 100644 index 00000000..27564aad --- /dev/null +++ b/migrations/20231020123433_add_requirement_components.sql @@ -0,0 +1,4 @@ +alter table requirements + drop column metadata; +alter table requirements + add components text[]; diff --git a/src/core/Flora/Import/Package.hs b/src/core/Flora/Import/Package.hs index acae3368..d76d86fe 100644 --- a/src/core/Flora/Import/Package.hs +++ b/src/core/Flora/Import/Package.hs @@ -36,6 +36,7 @@ import Data.Time (UTCTime) import Data.Vector (Vector) import Data.Vector qualified as Vector import Database.PostgreSQL.Simple (Connection) +import Distribution.Compat.NonEmptySet (toList) import Distribution.Compiler (CompilerFlavor (..)) import Distribution.Fields.ParseResult import Distribution.PackageDescription (CondBranch (..), CondTree (condTreeData), Condition (CNot), ConfVar, UnqualComponentName, allLibraries, unPackageName, unUnqualComponentName) @@ -83,9 +84,7 @@ import Flora.Model.Release.Types import Flora.Model.Release.Update qualified as Update import Flora.Model.Requirement ( Requirement (..) - , RequirementMetadata (..) , deterministicRequirementId - , flag ) import Flora.Model.User @@ -409,14 +408,14 @@ extractLibrary extractLibrary package repository = genericComponentExtractor Component.Library - (^. #libName % to getLibName) + (^. #libName % to (getLibName package.name)) (^. #libBuildInfo % #targetBuildDepends) package repository - where - getLibName :: LibraryName -> Text - getLibName LMainLibName = display package.name - getLibName (LSubLibName lname) = T.pack $ unUnqualComponentName lname + +getLibName :: PackageName -> LibraryName -> Text +getLibName pname LMainLibName = display pname +getLibName _ (LSubLibName lname) = T.pack $ unUnqualComponentName lname extractForeignLib :: Package @@ -548,7 +547,7 @@ buildDependency -> ComponentId -> Cabal.Dependency -> ImportDependency -buildDependency package (repository, repositoryPackages) packageComponentId (Cabal.Dependency depName versionRange _) = +buildDependency package (repository, repositoryPackages) packageComponentId (Cabal.Dependency depName versionRange libs) = let name = depName & unPackageName & pack & PackageName namespace = chooseNamespace name repository repositoryPackages packageId = deterministicPackageId namespace name @@ -564,7 +563,7 @@ buildDependency package (repository, repositoryPackages) packageComponentId (Cab , packageComponentId , packageId , requirement = display . prettyShow $ versionRange - , metadata = RequirementMetadata{flag = Nothing} + , components = fmap (getLibName name) . Vector.fromList $ toList libs } in ImportDependency{package = dependencyPackage, requirement} diff --git a/src/core/Flora/Model/Package/Query.hs b/src/core/Flora/Model/Package/Query.hs index f09bdd46..dd048929 100644 --- a/src/core/Flora/Model/Package/Query.hs +++ b/src/core/Flora/Model/Package/Query.hs @@ -161,6 +161,7 @@ packageDependentsWithLatestVersionQuery = SELECT DISTINCT p."namespace" , p."name" , '' + , array[]::text[] , max(r."version") , r.synopsis as "synopsis" , r.license as "license" @@ -224,7 +225,7 @@ getAllRequirementsQuery :: Query getAllRequirementsQuery = [sql| with requirements as ( - select distinct p1.component_type, p1.component_name, p0.namespace, p0.name, r0.requirement + select distinct p1.component_type, p1.component_name, p0.namespace, p0.name, r0.requirement, r0.components from requirements as r0 inner join packages as p0 on p0.package_id = r0.package_id inner join package_components as p1 on p1.package_component_id = r0.package_component_id @@ -236,6 +237,7 @@ getAllRequirementsQuery = , req.namespace , req.name , req.requirement + , req.components , r3.version as "dependency_latest_version" , r3.synopsis as "dependency_latest_synopsis" , r3.license as "dependency_latest_license" @@ -243,7 +245,7 @@ getAllRequirementsQuery = inner join packages as p2 on p2.namespace = req.namespace and p2.name = req.name inner join releases as r3 on r3.package_id = p2.package_id where r3.version = (select max(version) from releases where package_id = p2.package_id) - group by req.component_type, req.component_name, req.namespace, req.name, req.requirement, r3.version, r3.synopsis, r3.license + group by req.component_type, req.component_name, req.namespace, req.name, req.requirement, req.components, r3.version, r3.synopsis, r3.license order by req.component_type, req.component_name desc |] diff --git a/src/core/Flora/Model/Package/Update.hs b/src/core/Flora/Model/Package/Update.hs index 7e012c84..9b1cb591 100644 --- a/src/core/Flora/Model/Package/Update.hs +++ b/src/core/Flora/Model/Package/Update.hs @@ -69,7 +69,7 @@ insertRequirement :: DB :> es => Requirement -> Eff es () insertRequirement = dbtToEff . insert @Requirement upsertRequirement :: DB :> es => Requirement -> Eff es () -upsertRequirement req = dbtToEff $ upsert @Requirement req [[field| metadata |], [field| requirement |]] +upsertRequirement req = dbtToEff $ upsert @Requirement req [[field| components |], [field| requirement |]] bulkInsertRequirements :: DB :> es => [Requirement] -> Eff es () bulkInsertRequirements requirements = diff --git a/src/core/Flora/Model/Requirement.hs b/src/core/Flora/Model/Requirement.hs index 5bbada50..8eea8ddd 100644 --- a/src/core/Flora/Model/Requirement.hs +++ b/src/core/Flora/Model/Requirement.hs @@ -1,7 +1,6 @@ module Flora.Model.Requirement where import Crypto.Hash.MD5 qualified as MD5 -import Data.Data import Data.Foldable (foldl') import Data.Map.Strict qualified as Map import Data.Text (Text) @@ -13,11 +12,9 @@ import Database.PostgreSQL.Entity.Types (GenericEntity, TableName) import Database.PostgreSQL.Simple (ToRow) import Database.PostgreSQL.Simple.FromField ( FromField - , fromField - , fromJSONField ) import Database.PostgreSQL.Simple.FromRow (FromRow (..)) -import Database.PostgreSQL.Simple.ToField (ToField, toField, toJSONField) +import Database.PostgreSQL.Simple.ToField (ToField) import Control.DeepSeq import Data.ByteString.Lazy (fromStrict) @@ -50,8 +47,8 @@ data Requirement = Requirement -- ^ Package that is being depended on , requirement :: Text -- ^ The human-readable version range expression of this requirement - , metadata :: RequirementMetadata - -- ^ Additional metadata, like flags + , components :: Vector Text + -- ^ Components that are depended on } deriving stock (Eq, Show, Generic) deriving anyclass (FromRow, ToRow, NFData, FromJSON, ToJSON) @@ -62,26 +59,12 @@ data Requirement = Requirement (Display) via ShowInstance Requirement -data RequirementMetadata = RequirementMetadata - { flag :: Maybe Text - } - deriving stock (Eq, Show, Generic, Typeable) - deriving anyclass (NFData) - deriving - (ToJSON, FromJSON) - via (CustomJSON '[FieldLabelModifier '[CamelToSnake]] RequirementMetadata) - -instance FromField RequirementMetadata where - fromField = fromJSONField - -instance ToField RequirementMetadata where - toField = toJSONField - -- | This datatype holds information about the latest version of a dependency data DependencyInfo = DependencyInfo { namespace :: Namespace , name :: PackageName , requirement :: Text + , components :: Vector Text , latestVersion :: Version , latestSynopsis :: Text , latestLicense :: SPDX.License @@ -96,6 +79,7 @@ data ComponentDependency' = ComponentDependency' , namespace :: Namespace , name :: PackageName , requirement :: Text + , components :: Vector Text , latestVersion :: Version , latestSynopsis :: Text , latestLicense :: SPDX.License diff --git a/src/web/FloraWeb/Components/PackageListItem.hs b/src/web/FloraWeb/Components/PackageListItem.hs index 4a961163..6059d1aa 100644 --- a/src/web/FloraWeb/Components/PackageListItem.hs +++ b/src/web/FloraWeb/Components/PackageListItem.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE OverloadedLists #-} + module FloraWeb.Components.PackageListItem ( packageListItem , requirementListItem @@ -6,7 +8,7 @@ module FloraWeb.Components.PackageListItem where import Data.Foldable (traverse_) -import Data.List (sortOn) +import Data.List (intersperse, sortOn) import Data.Map qualified as Map import Data.Text (Text) import Data.Text.Display (display) @@ -53,13 +55,25 @@ requirementListItem allComponentDeps = traverse_ componentListItems componentDeps componentListItems :: DependencyInfo -> FloraHTML -componentListItems DependencyInfo{namespace, name = packageName, latestSynopsis, requirement, latestLicense} = do +componentListItems DependencyInfo{namespace, name = packageName, latestSynopsis, requirement, latestLicense, components} = do let href = href_ ("/packages/" <> display namespace <> "/" <> display packageName) + component_ = p_ [class_ "package-list-item__component"] . toHtml li_ [class_ "package-list-item"] $ a_ [href, class_ ""] $ do - h4_ [class_ "package-list-item__name"] $ + h4_ [class_ "package-list-item__name"] $ do strong_ [class_ ""] . toHtml $ display namespace <> "/" <> display packageName + case components of + [name] + | name == display packageName -> pure () + | otherwise -> ":" >> component_ name + -- The empty case should never happen but displaying pkg:{} will indicate + -- something has gone wrong. + _ -> do + ":{" + sequence_ . intersperse (toHtml @Text ", ") $ + component_ <$> Vector.toList components + "}" p_ [class_ "package-list-item__synopsis"] $ toHtml latestSynopsis div_ [class_ "package-list-item__metadata"] $ do span_ [class_ "package-list-item__license"] $ do diff --git a/test/fixtures/Cabal/b.cabal b/test/fixtures/Cabal/b.cabal index 9490c463..155ee41b 100644 --- a/test/fixtures/Cabal/b.cabal +++ b/test/fixtures/Cabal/b.cabal @@ -19,7 +19,7 @@ library -- other-modules: -- other-extensions: build-depends: base >=4 && <5 - --, sublib + , sublib -- hs-source-dirs: default-language: Haskell2010 From 60132bb519433caf1fe46f79ada23cc0c31a0b87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 20:47:30 +0100 Subject: [PATCH 20/40] Bump browserify-sign from 4.2.1 to 4.2.2 in /docs (#474) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/yarn.lock | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index ae68107a..7177115b 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -2866,7 +2866,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== -bn.js@^5.0.0, bn.js@^5.1.1: +bn.js@^5.0.0, bn.js@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== @@ -2990,7 +2990,7 @@ browserify-des@^1.0.0: inherits "^2.0.1" safe-buffer "^5.1.2" -browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: +browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== @@ -2999,19 +2999,19 @@ browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: randombytes "^2.0.1" browserify-sign@^4.0.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3" - integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg== + version "4.2.2" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.2.tgz#e78d4b69816d6e3dd1c747e64e9947f9ad79bc7e" + integrity sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg== dependencies: - bn.js "^5.1.1" - browserify-rsa "^4.0.1" + bn.js "^5.2.1" + browserify-rsa "^4.1.0" create-hash "^1.2.0" create-hmac "^1.1.7" - elliptic "^6.5.3" + elliptic "^6.5.4" inherits "^2.0.4" - parse-asn1 "^5.1.5" - readable-stream "^3.6.0" - safe-buffer "^5.2.0" + parse-asn1 "^5.1.6" + readable-stream "^3.6.2" + safe-buffer "^5.2.1" browserify-zlib@^0.2.0: version "0.2.0" @@ -4030,7 +4030,7 @@ electron-to-chromium@^1.4.477: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.480.tgz#40e32849ca50bc23ce29c1516c5adb3fddac919d" integrity sha512-IXTgg+bITkQv/FLP9FjX6f9KFCs5hQWeh5uNSKxB9mqYj/JXhHDbu+ekS43LVvbkL3eW6/oZy4+r9Om6lan1Uw== -elliptic@^6.5.3: +elliptic@^6.5.3, elliptic@^6.5.4: version "6.5.4" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== @@ -6246,7 +6246,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0, parse-asn1@^5.1.5: +parse-asn1@^5.0.0, parse-asn1@^5.1.6: version "5.1.6" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== @@ -7081,7 +7081,7 @@ readable-stream@^2.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^3.0.6, readable-stream@^3.5.0, readable-stream@^3.6.0: +readable-stream@^3.0.6, readable-stream@^3.5.0, readable-stream@^3.6.0, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -7389,7 +7389,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== From debb3e410e9c847617cd1a2a859259654c5ed346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Tue, 31 Oct 2023 10:09:32 +0100 Subject: [PATCH 21/40] [FLORA-460] Lower the default DB pool connection number (#475) --- environment.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.sh b/environment.sh index a0be0c3d..141c9925 100755 --- a/environment.sh +++ b/environment.sh @@ -7,7 +7,7 @@ export FLORA_DB_PORT="5432" export FLORA_DB_USER="postgres" export FLORA_DB_PASSWORD="postgres" export FLORA_DB_DATABASE="flora_dev" -export FLORA_DB_POOL_CONNECTIONS="96" +export FLORA_DB_POOL_CONNECTIONS="50" export FLORA_DB_TIMEOUT="10" export FLORA_DB_SSLMODE="allow" export FLORA_DB_PARAMETERS="?sslmode=verify-ca" From 2c34a47ffde53e5e76d9701f5136230ddb63e359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Tue, 31 Oct 2023 17:42:55 +0100 Subject: [PATCH 22/40] [FLORA-388] Reverse dependencies search bar (#476) * [FLORA-388] Reverse dependencies search bar --------- Co-authored-by: Dylan Thinnes --- .github/mergify.yml | 2 +- CHANGELOG.md | 1 + assets/css/2-components/5-primary-search.css | 61 ++++++++ .../css/2-components/6-secondary-search.css | 60 +++++++ assets/css/3-screens/4-front-page.css | 59 ------- assets/css/styles.css | 8 +- flora.cabal | 6 +- src/core/Flora/Model/Package/Query.hs | 146 ++++++++++++++---- src/core/Flora/Search.hs | 9 +- src/web/FloraWeb/Components/Icons.hs | 57 +++++++ src/web/FloraWeb/Components/MainSearchBar.hs | 23 +++ src/web/FloraWeb/Components/PaginationNav.hs | 4 +- src/web/FloraWeb/Components/SlimSearchBar.hs | 30 ++++ src/web/FloraWeb/Components/Utils.hs | 2 +- src/web/FloraWeb/Links.hs | 10 +- src/web/FloraWeb/Pages/Routes/Packages.hs | 2 + src/web/FloraWeb/Pages/Server/Packages.hs | 40 +++-- src/web/FloraWeb/Pages/Templates/Packages.hs | 103 ++++++------ .../Pages/Templates/Pages/Categories/Show.hs | 2 +- .../FloraWeb/Pages/Templates/Pages/Home.hs | 33 +--- .../Pages/Templates/Pages/Packages.hs | 4 +- .../FloraWeb/Pages/Templates/Pages/Search.hs | 11 +- test/Flora/PackageSpec.hs | 2 +- 23 files changed, 465 insertions(+), 210 deletions(-) create mode 100644 assets/css/2-components/5-primary-search.css create mode 100644 assets/css/2-components/6-secondary-search.css create mode 100644 src/web/FloraWeb/Components/Icons.hs create mode 100644 src/web/FloraWeb/Components/MainSearchBar.hs create mode 100644 src/web/FloraWeb/Components/SlimSearchBar.hs diff --git a/.github/mergify.yml b/.github/mergify.yml index fe7cbd69..6e8b56c9 100644 --- a/.github/mergify.yml +++ b/.github/mergify.yml @@ -11,7 +11,7 @@ pull_request_rules: conditions: - label=merge me - 'check-success=Frontend_tests' - - 'check-success=Backend_tests' + - 'check-success~=.*Backend_tests.*' # - '#approved-reviews-by>=1' # merge+squash strategy - actions: diff --git a/CHANGELOG.md b/CHANGELOG.md index 557898b7..71a50e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Add a page on namespaces in the documentation ([#451](/~https://github.com/flora-pm/flora-server/pull/451)) * Add initial support for hosting package tarballs ([#452](/~https://github.com/flora-pm/flora-server/pull/452)) * Show depended on components in dependencies page ([#464](/~https://github.com/flora-pm/flora-server/pull/464)) +* Add search bar for reverse dependencies ([#476](/~https://github.com/flora-pm/flora-server/pull/476)) ## 1.0.13 -- 2023-09-17 * Exclude deprecated releases from latest versions and search ([#373](/~https://github.com/flora-pm/flora-server/pull/373)) diff --git a/assets/css/2-components/5-primary-search.css b/assets/css/2-components/5-primary-search.css new file mode 100644 index 00000000..46ad7145 --- /dev/null +++ b/assets/css/2-components/5-primary-search.css @@ -0,0 +1,61 @@ +/* stylelint-disable selector-class-pattern */ +/* stylelint-disable declaration-block-no-redundant-longhand-properties */ + +.main-search { + background-color: var(--search-bar-background-color); + border-radius: 0.75rem; + border-width: 2px; + display: flex; + font-size: 1.5rem; + justify-content: center; + line-height: 2rem; + max-width: 28rem; + outline-offset: -2px; + overflow: hidden; + padding: 0.5rem; + + .search-bar { + background-color: var(--search-bar-background-color); + color: var(--search-bar-color); + display: block; + margin-left: 0.5rem; + font-size: 1.5rem; + line-height: 2rem; + padding: 0.5rem; + flex-grow: 1; + min-width: 0; + } + + .search-bar:hover { + background-color: var(--search-bar-background-hover-color); + } + + .search-bar:focus { + background-color: var(--search-bar-background-focus-color); + outline: 2px solid transparent; + outline-offset: 2px; + } +} + +.main-search:focus-within { + border-color: var(--search-bar-focus-border-color); + transition-property: box-shadow; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 400ms; + + /* offset-x | offset-y | blur-radius | spread-radius | color */ + box-shadow: 5px 5px 5px 2px var(--search-bar-focus-border-color); +} + +.main-search button { + margin-bottom: 1.25rem; + margin-right: 1rem; + margin-top: 1.25rem; + + svg { + width: 1.5rem; + height: 1.5rem; + margin-top: auto; + margin-bottom: auto; + } +} diff --git a/assets/css/2-components/6-secondary-search.css b/assets/css/2-components/6-secondary-search.css new file mode 100644 index 00000000..6bef98a1 --- /dev/null +++ b/assets/css/2-components/6-secondary-search.css @@ -0,0 +1,60 @@ +/* stylelint-disable selector-class-pattern */ +/* stylelint-disable declaration-block-no-redundant-longhand-properties */ + +.secondary-search { + background-color: var(--search-bar-background-color); + border-radius: 0.75rem; + border-width: 2px; + display: flex; + font-size: 1rem; + justify-content: center; + line-height: 2rem; + max-width: 20rem; + outline-offset: -2px; + overflow: hidden; + padding: 0.3rem; + + .search-bar { + background-color: var(--search-bar-background-color); + color: var(--search-bar-color); + display: block; + font-size: 1.5rem; + line-height: 2rem; + padding: 0.5rem; + flex-grow: 1; + min-width: 0; + } + + .search-bar:hover { + background-color: var(--search-bar-background-hover-color); + } + + .search-bar:focus { + background-color: var(--search-bar-background-focus-color); + outline: transparent solid 2px; + outline-offset: 2px; + } +} + +.secondary-search:focus-within { + border-color: var(--search-bar-focus-border-color); + transition-property: box-shadow; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 300ms; + + /* offset-x | offset-y | blur-radius | spread-radius | color */ + box-shadow: 4px 4px 4px 1px var(--search-bar-focus-border-color); +} + +.secondary-search button { + margin-bottom: 1.25rem; + margin-right: 1rem; + margin-top: 1.25rem; + + svg { + width: 1em; + height: 1em; + margin-top: auto; + margin-bottom: auto; + } +} diff --git a/assets/css/3-screens/4-front-page.css b/assets/css/3-screens/4-front-page.css index 9515e534..1cc00908 100644 --- a/assets/css/3-screens/4-front-page.css +++ b/assets/css/3-screens/4-front-page.css @@ -13,65 +13,6 @@ text-align: center; } -.main-search { - background-color: var(--search-bar-background-color); - border-radius: 0.75rem; - border-width: 2px; - display: flex; - font-size: 1.5rem; - justify-content: center; - line-height: 2rem; - max-width: 28rem; - outline-offset: -2px; - overflow: hidden; - padding: 0.5rem; - - .search-bar { - background-color: var(--search-bar-background-color); - color: var(--search-bar-color); - display: block; - margin-left: 0.5rem; - font-size: 1.5rem; - line-height: 2rem; - padding: 0.5rem; - flex-grow: 1; - min-width: 0; - } - - .search-bar:hover { - background-color: var(--search-bar-background-hover-color); - } - - .search-bar:focus { - background-color: var(--search-bar-background-focus-color); - outline: 2px solid transparent; - outline-offset: 2px; - } -} - -.main-search:focus-within { - border-color: var(--search-bar-focus-border-color); - transition-property: box-shadow; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 400ms; - - /* offset-x | offset-y | blur-radius | spread-radius | color */ - box-shadow: 5px 5px 5px 2px var(--search-bar-focus-border-color); -} - -.main-search button { - margin-bottom: 1.25rem; - margin-right: 1rem; - margin-top: 1.25rem; - - svg { - width: 1.5rem; - height: 1.5rem; - margin-top: auto; - margin-bottom: auto; - } -} - section#main-page-buttons { border-top: 1px solid var(--text-color); border-color: var(--navbar-background-color); diff --git a/assets/css/styles.css b/assets/css/styles.css index 3932e3b5..d69dcbbd 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -8,6 +8,8 @@ @import "2-components/2-package-component.css"; @import "2-components/3-breadcrumb.css"; @import "2-components/4-license.css"; +@import "2-components/5-primary-search.css"; +@import "2-components/6-secondary-search.css"; @import "3-screens/1-package/1-package.css"; @import "3-screens/1-package/2-release-changelog.css"; @@ -80,13 +82,13 @@ a { display: block; + font-size: 1.25rem; + line-height: 1.75rem; margin-top: 1rem; margin-bottom: 1rem; padding-left: 1rem; padding-top: 0.25rem; padding-bottom: 0.25rem; - font-size: 1.25rem; - line-height: 1.75rem; } a:hover { @@ -222,8 +224,10 @@ text-decoration: underline; } +/* offset-x | offset-y | blur-radius | spread-radius | color */ .exact-match { box-shadow: 0 5px 10px 0 rgb(0 0 0 / 50%); + border-radius: 6px; } .package-count { diff --git a/flora.cabal b/flora.cabal index 2729aa2f..fed71ac6 100644 --- a/flora.cabal +++ b/flora.cabal @@ -39,7 +39,6 @@ common common-extensions OverloadedStrings PackageImports PolyKinds - QuasiQuotes RecordWildCards StrictData TypeFamilies @@ -157,6 +156,7 @@ library , directory , effectful-core , envparse + , extra , filepath , http-api-data , http-media @@ -223,10 +223,13 @@ library flora-web FloraWeb.Components.CategoryCard FloraWeb.Components.Footer FloraWeb.Components.Header + FloraWeb.Components.Icons + FloraWeb.Components.MainSearchBar FloraWeb.Components.Navbar FloraWeb.Components.PackageListHeader FloraWeb.Components.PackageListItem FloraWeb.Components.PaginationNav + FloraWeb.Components.SlimSearchBar FloraWeb.Components.Utils FloraWeb.Components.VersionListHeader FloraWeb.Embedded @@ -280,6 +283,7 @@ library flora-web , deriving-aeson , effectful , effectful-core + , extra , flora , flora-jobs , haddock-library diff --git a/src/core/Flora/Model/Package/Query.hs b/src/core/Flora/Model/Package/Query.hs index dd048929..18298c0b 100644 --- a/src/core/Flora/Model/Package/Query.hs +++ b/src/core/Flora/Model/Package/Query.hs @@ -81,7 +81,21 @@ getAllPackageDependents => Namespace -> PackageName -> Eff es (Vector Package) -getAllPackageDependents namespace packageName = dbtToEff $ query Select packageDependentsQuery (namespace, packageName) +getAllPackageDependents namespace packageName = + dbtToEff $ query Select packageDependentsQuery (namespace, packageName) + +getPackageDependentsByName + :: DB :> es + => Namespace + -> PackageName + -> Text + -> Eff es (Vector Package) +getPackageDependentsByName namespace packageName searchString = + dbtToEff $ + query + Select + searchPackageDependentsQuery + (namespace, packageName, searchString) -- | This function gets the first 6 dependents of a package getPackageDependents :: DB :> es => Namespace -> PackageName -> Eff es (Vector Package) @@ -89,14 +103,26 @@ getPackageDependents namespace packageName = dbtToEff $ query Select q (namespac where q = packageDependentsQuery <> " LIMIT 6" -getNumberOfPackageDependents :: DB :> es => Namespace -> PackageName -> Eff es Word -getNumberOfPackageDependents namespace packageName = - dbtToEff $ do - (result :: Maybe (Only Int)) <- - queryOne Select numberOfPackageDependentsQuery (namespace, packageName) - case result of - Just (Only n) -> pure $ fromIntegral n - Nothing -> pure 0 +getNumberOfPackageDependents + :: DB :> es + => Namespace + -> PackageName + -> Maybe Text + -> Eff es Word +getNumberOfPackageDependents namespace packageName mbSearchString = do + case mbSearchString of + Nothing -> + dbtToEff $ do + (result :: Maybe (Only Int)) <- queryOne Select numberOfPackageDependentsQuery (namespace, packageName) + case result of + Just (Only n) -> pure $ fromIntegral n + Nothing -> pure 0 + Just searchString -> + dbtToEff $ do + (result :: Maybe (Only Int)) <- queryOne Select searchNumberOfPackageDependentsQuery (namespace, packageName, searchString) + case result of + Just (Only n) -> pure $ fromIntegral n + Nothing -> pure 0 numberOfPackageDependentsQuery :: Query numberOfPackageDependentsQuery = @@ -109,6 +135,19 @@ numberOfPackageDependentsQuery = AND dep."name" = ? |] +searchNumberOfPackageDependentsQuery :: Query +searchNumberOfPackageDependentsQuery = + [sql| + SELECT DISTINCT count(p."package_id") + FROM "packages" AS p + INNER JOIN "dependents" AS dep + ON p."package_id" = dep."dependent_id" + WHERE dep."namespace" = ? + AND dep."name" = ? + AND ? <% p."name" + |] + +-- | Fetch the dependents of a package. packageDependentsQuery :: Query packageDependentsQuery = [sql| @@ -127,16 +166,27 @@ packageDependentsQuery = AND dep."name" = ? |] +searchPackageDependentsQuery :: Query +searchPackageDependentsQuery = + packageDependentsQuery <> " AND ? <% p.name" + getAllPackageDependentsWithLatestVersion :: DB :> es => Namespace -> PackageName -> (Word, Word) + -> Maybe Text -> Eff es (Vector DependencyInfo) -getAllPackageDependentsWithLatestVersion namespace packageName (offset, limit) = - dbtToEff $ query Select q (namespace, packageName, offset, limit) - where - q = packageDependentsWithLatestVersionQuery <> " OFFSET ? LIMIT ?" +getAllPackageDependentsWithLatestVersion namespace packageName (offset, limit) mSearchString = + case mSearchString of + Nothing -> + dbtToEff $ query Select q (namespace, packageName, offset, limit) + where + q = packageDependentsWithLatestVersionQuery <> " OFFSET ? LIMIT ?" + Just searchString -> + dbtToEff $ query Select q (namespace, packageName, searchString, offset, limit) + where + q = searchPackageDependentsWithLatestVersionQuery <> " OFFSET ? LIMIT ?" getPackageDependentsWithLatestVersion :: (DB :> es, Log :> es, Time :> es) @@ -158,22 +208,60 @@ getPackageDependentsWithLatestVersion namespace packageName = do packageDependentsWithLatestVersionQuery :: Query packageDependentsWithLatestVersionQuery = [sql| - SELECT DISTINCT p."namespace" - , p."name" - , '' - , array[]::text[] - , max(r."version") - , r.synopsis as "synopsis" - , r.license as "license" - FROM "packages" AS p - INNER JOIN "dependents" AS dep - ON p."package_id" = dep."dependent_id" - INNER JOIN "releases" AS r - ON r."package_id" = p."package_id" - WHERE dep."namespace" = ? - AND dep."name" = ? - GROUP BY (p.namespace, p.name, synopsis, license) - ORDER BY p.namespace DESC +WITH dependents AS ( + SELECT row_number() OVER ( + PARTITION BY p.name + ORDER BY r.version DESC) AS rank + , p.namespace + , p.name + , r.version + , r.synopsis + , r.license + FROM packages AS p + INNER JOIN dependents AS dep ON p.package_id = dep.dependent_id + INNER JOIN releases AS r ON r.package_id = p.package_id + WHERE dep.namespace = ? + AND dep.name = ? +) + +SELECT d.namespace + , d.name + , '' + , (ARRAY[]::text[]) + , d.version + , d.synopsis + , d.license +FROM dependents AS d +WHERE rank = 1 + |] + +searchPackageDependentsWithLatestVersionQuery :: Query +searchPackageDependentsWithLatestVersionQuery = + [sql| +WITH dependents AS ( + SELECT row_number() OVER ( + PARTITION BY p.name + ORDER BY r.version DESC) AS rank + , p.namespace + , p.name + , r.version + , r.synopsis + , r.license + FROM packages AS p + INNER JOIN dependents AS dep ON p.package_id = dep.dependent_id + INNER JOIN releases AS r ON r.package_id = p.package_id + WHERE dep.namespace = ? AND dep.name = ? AND ? <% p."name" +) + +SELECT d.namespace + , d.name + , '' + , (ARRAY[]::text[]) + , d.version + , d.synopsis + , d.license +FROM dependents AS d +WHERE rank = 1 |] getComponentById :: DB :> es => ComponentId -> Eff es (Maybe PackageComponent) diff --git a/src/core/Flora/Search.hs b/src/core/Flora/Search.hs index c59ec650..cf0feaa1 100644 --- a/src/core/Flora/Search.hs +++ b/src/core/Flora/Search.hs @@ -21,14 +21,19 @@ data SearchAction = ListAllPackages | ListAllPackagesInNamespace Namespace | SearchPackages Text - | DependentsOf Namespace PackageName + | DependentsOf Namespace PackageName (Maybe Text) deriving (Eq, Ord, Show) instance Display SearchAction where displayBuilder ListAllPackages = "Packages" displayBuilder (ListAllPackagesInNamespace namespace) = "Packages in " <> displayBuilder namespace displayBuilder (SearchPackages title) = "\"" <> Builder.fromText title <> "\"" - displayBuilder (DependentsOf namespace packageName) = "Dependents of " <> displayBuilder namespace <> "/" <> displayBuilder packageName + displayBuilder (DependentsOf namespace packageName mbSearchString) = + "Dependents of " + <> displayBuilder namespace + <> "/" + <> displayBuilder packageName + <> foldMap (\searchString -> " \"" <> Builder.fromText searchString <> "\"") mbSearchString searchPackageByName :: (DB :> es, Log :> es, Time :> es) diff --git a/src/web/FloraWeb/Components/Icons.hs b/src/web/FloraWeb/Components/Icons.hs new file mode 100644 index 00000000..0b4a4847 --- /dev/null +++ b/src/web/FloraWeb/Components/Icons.hs @@ -0,0 +1,57 @@ +{-# LANGUAGE QuasiQuotes #-} + +module FloraWeb.Components.Icons + ( usageInstructionTooltip + , chevronRightOutline + , pen + , lookingGlass + ) where + +import Data.Text (Text) +import Lucid +import Lucid.Svg + ( d_ + , fill_ + , path_ + , stroke_ + , stroke_linecap_ + , stroke_linejoin_ + , stroke_width_ + , viewBox_ + ) +import PyF + +import FloraWeb.Pages.Templates.Types (FloraHTML) + +usageInstructionTooltip :: FloraHTML +usageInstructionTooltip = + toHtmlRaw @Text + [str| + + + +|] + +chevronRightOutline :: FloraHTML +chevronRightOutline = + toHtmlRaw @Text + [str| + + + +|] + +pen :: FloraHTML +pen = + toHtmlRaw @Text + [str| + + + +|] + +lookingGlass :: FloraHTML +lookingGlass = + button_ [type_ "submit"] $ + svg_ [xmlns_ "http://www.w3.org/2000/svg", style_ "color: gray", fill_ "none", viewBox_ "0 0 24 24", stroke_ "currentColor"] $ + path_ [stroke_linecap_ "round", stroke_linejoin_ "round", stroke_width_ "2", d_ "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"] diff --git a/src/web/FloraWeb/Components/MainSearchBar.hs b/src/web/FloraWeb/Components/MainSearchBar.hs new file mode 100644 index 00000000..f31a63ac --- /dev/null +++ b/src/web/FloraWeb/Components/MainSearchBar.hs @@ -0,0 +1,23 @@ +module FloraWeb.Components.MainSearchBar where + +import FloraWeb.Components.Icons +import FloraWeb.Pages.Templates.Types (FloraHTML) +import Lucid + +mainSearchBar :: FloraHTML +mainSearchBar = + form_ [action_ "/search", method_ "GET"] $ do + div_ [class_ "main-search"] $ do + label_ [for_ "search"] "" + input_ + [ class_ + "search-bar" + , type_ "search" + , id_ "search" + , name_ "q" + , placeholder_ "Find a package" + , value_ "" + , tabindex_ "1" + , autofocus_ + ] + lookingGlass diff --git a/src/web/FloraWeb/Components/PaginationNav.hs b/src/web/FloraWeb/Components/PaginationNav.hs index dcf1f4ad..4e432746 100644 --- a/src/web/FloraWeb/Components/PaginationNav.hs +++ b/src/web/FloraWeb/Components/PaginationNav.hs @@ -49,8 +49,8 @@ mkURL (ListAllPackagesInNamespace namespace) pageNumber = "/" <> toUrlPiece (Links.namespaceLink namespace pageNumber) mkURL (SearchPackages searchTerm) pageNumber = "/" <> toUrlPiece (Links.packageSearchLink searchTerm pageNumber) -mkURL (DependentsOf namespace packageName) pageNumber = - "/" <> toUrlPiece (Links.packageDependents namespace packageName pageNumber) +mkURL (DependentsOf namespace packageName mbSearchString) pageNumber = + "/" <> toUrlPiece (Links.packageDependents namespace packageName pageNumber mbSearchString) paginate :: Word diff --git a/src/web/FloraWeb/Components/SlimSearchBar.hs b/src/web/FloraWeb/Components/SlimSearchBar.hs new file mode 100644 index 00000000..0d4685de --- /dev/null +++ b/src/web/FloraWeb/Components/SlimSearchBar.hs @@ -0,0 +1,30 @@ +module FloraWeb.Components.SlimSearchBar (slimSearchBar, SearchBarOptions (..)) where + +import Data.Text (Text) +import FloraWeb.Components.Icons +import FloraWeb.Pages.Templates.Types (FloraHTML) +import Lucid + +data SearchBarOptions = SearchBarOptions + { actionUrl :: Text + , placeholder :: Text + , value :: Text + } + +slimSearchBar :: SearchBarOptions -> FloraHTML +slimSearchBar SearchBarOptions{actionUrl, placeholder, value} = + form_ [action_ actionUrl, method_ "GET"] $! do + div_ [class_ "secondary-search"] $ do + label_ [for_ "search"] "" + input_ + [ class_ + "search-bar" + , type_ "search" + , id_ "search" + , name_ "q" + , placeholder_ placeholder + , value_ value + , tabindex_ "1" + , autofocus_ + ] + lookingGlass diff --git a/src/web/FloraWeb/Components/Utils.hs b/src/web/FloraWeb/Components/Utils.hs index 21e31556..777694d7 100644 --- a/src/web/FloraWeb/Components/Utils.hs +++ b/src/web/FloraWeb/Components/Utils.hs @@ -2,7 +2,7 @@ module FloraWeb.Components.Utils where import Data.Text (Text) import FloraWeb.Pages.Templates.Types (FloraHTML) -import Lucid (Attribute, a_, class_, href_, role_, toHtml) +import Lucid import Lucid.Base (makeAttribute) text :: Text -> FloraHTML diff --git a/src/web/FloraWeb/Links.hs b/src/web/FloraWeb/Links.hs index 7529b679..b2b7f767 100644 --- a/src/web/FloraWeb/Links.hs +++ b/src/web/FloraWeb/Links.hs @@ -82,14 +82,20 @@ packageDependencies namespace packageName version = /: packageName /: version -packageDependents :: Namespace -> PackageName -> Positive Word -> Link -packageDependents namespace packageName pageNumber = +packageDependents + :: Namespace + -> PackageName + -> Positive Word + -> Maybe Text + -> Link +packageDependents namespace packageName pageNumber search = links // Web.packages // Web.showDependents /: namespace /: packageName /: Just pageNumber + /: search packageVersions :: Namespace -> PackageName -> Link packageVersions namespace packageName = diff --git a/src/web/FloraWeb/Pages/Routes/Packages.hs b/src/web/FloraWeb/Pages/Routes/Packages.hs index bc57cdf7..73e54008 100644 --- a/src/web/FloraWeb/Pages/Routes/Packages.hs +++ b/src/web/FloraWeb/Pages/Routes/Packages.hs @@ -38,6 +38,7 @@ data Routes' mode = Routes' :> Capture "package" PackageName :> "dependents" :> QueryParam "page" (Positive Word) + :> QueryParam "q" Text :> Get '[HTML] (Html ()) , showVersionDependents :: mode @@ -46,6 +47,7 @@ data Routes' mode = Routes' :> Capture "version" Version :> "dependents" :> QueryParam "page" (Positive Word) + :> QueryParam "q" Text :> Get '[HTML] (Html ()) , showDependencies :: mode diff --git a/src/web/FloraWeb/Pages/Server/Packages.hs b/src/web/FloraWeb/Pages/Server/Packages.hs index 98adcf73..3316b6d6 100644 --- a/src/web/FloraWeb/Pages/Server/Packages.hs +++ b/src/web/FloraWeb/Pages/Server/Packages.hs @@ -101,7 +101,7 @@ showPackageVersion namespace packageName mversion = do dependents <- Query.getPackageDependents namespace packageName releaseDependencies <- Query.getRequirements release.releaseId categories <- Query.getPackageCategories package.packageId - numberOfDependents <- Query.getNumberOfPackageDependents namespace packageName + numberOfDependents <- Query.getNumberOfPackageDependents namespace packageName Nothing numberOfDependencies <- Query.getNumberOfPackageRequirements release.releaseId let templateEnv = @@ -141,17 +141,30 @@ showPackageVersion namespace packageName mversion = do numberOfDependencies categories -showDependentsHandler :: Namespace -> PackageName -> Maybe (Positive Word) -> FloraPage (Html ()) -showDependentsHandler namespace packageName mPage = do +showDependentsHandler + :: Namespace + -> PackageName + -> Maybe (Positive Word) + -> Maybe Text + -> FloraPage (Html ()) +showDependentsHandler namespace packageName mPage mSearch = do package <- guardThatPackageExists namespace packageName (\_ _ -> web404) releases <- Query.getAllReleases package.packageId let latestRelease = maximumBy (compare `on` (.version)) releases - showVersionDependentsHandler namespace packageName latestRelease.version mPage + showVersionDependentsHandler namespace packageName latestRelease.version mPage mSearch -showVersionDependentsHandler :: Namespace -> PackageName -> Version -> Maybe (Positive Word) -> FloraPage (Html ()) -showVersionDependentsHandler namespace packageName version Nothing = - showVersionDependentsHandler namespace packageName version (Just $ PositiveUnsafe 1) -showVersionDependentsHandler namespace packageName version (Just pageNumber) = do +showVersionDependentsHandler + :: Namespace + -> PackageName + -> Version + -> Maybe (Positive Word) + -> Maybe Text + -> FloraPage (Html ()) +showVersionDependentsHandler namespace packageName version Nothing mSearch = + showVersionDependentsHandler namespace packageName version (Just $ PositiveUnsafe 1) mSearch +showVersionDependentsHandler namespace packageName version pageNumber (Just "") = + showVersionDependentsHandler namespace packageName version pageNumber Nothing +showVersionDependentsHandler namespace packageName version (Just pageNumber) mSearch = do session <- getSession templateEnv' <- fromSession session defaultTemplateEnv package <- guardThatPackageExists namespace packageName (\_ _ -> web404) @@ -161,8 +174,14 @@ showVersionDependentsHandler namespace packageName version (Just pageNumber) = d { title = display namespace <> "/" <> display packageName , description = "Dependents of " <> display namespace <> display packageName } - results <- Query.getAllPackageDependentsWithLatestVersion namespace packageName (fromPage pageNumber) - totalDependents <- Query.getNumberOfPackageDependents namespace packageName + results <- + Query.getAllPackageDependentsWithLatestVersion + namespace + packageName + (fromPage pageNumber) + mSearch + + totalDependents <- Query.getNumberOfPackageDependents namespace packageName mSearch render templateEnv $ Package.showDependents namespace @@ -171,6 +190,7 @@ showVersionDependentsHandler namespace packageName version (Just pageNumber) = d totalDependents results pageNumber + mSearch showDependenciesHandler :: Namespace -> PackageName -> FloraPage (Html ()) showDependenciesHandler namespace packageName = do diff --git a/src/web/FloraWeb/Pages/Templates/Packages.hs b/src/web/FloraWeb/Pages/Templates/Packages.hs index 7858005d..842c05de 100644 --- a/src/web/FloraWeb/Pages/Templates/Packages.hs +++ b/src/web/FloraWeb/Pages/Templates/Packages.hs @@ -1,11 +1,12 @@ -{-# LANGUAGE QuasiQuotes #-} - module FloraWeb.Pages.Templates.Packages where import Control.Monad (when) +import Control.Monad.Extra (whenJust) import Control.Monad.Reader (ask) +import Data.Foldable (fold, forM_) import Data.List qualified as List import Data.Map.Strict qualified as Map +import Data.Maybe (fromJust, fromMaybe, isJust) import Data.Positive import Data.Text (Text) import Data.Text qualified as Text @@ -16,29 +17,28 @@ import Data.Vector (Vector) import Data.Vector qualified as Vector import Data.Vector.Algorithms.Intro qualified as MVector import Distribution.Orphans () +import Distribution.Pretty (pretty) import Distribution.SPDX.License qualified as SPDX import Distribution.Types.Flag (PackageFlag (..)) import Distribution.Types.Flag qualified as Flag import Distribution.Types.Version (Version, mkVersion, versionNumbers) import Lucid import Lucid.Base -import PyF import Servant (ToHttpApiData (..)) import Text.PrettyPrint (Doc, hcat, render) import Text.PrettyPrint qualified as PP -import Data.Foldable (fold) -import Data.Maybe (fromJust, isJust) -import Distribution.Pretty (pretty) import Flora.Environment (FeatureEnv (..)) import Flora.Model.Category.Types import Flora.Model.Package import Flora.Model.Release.Types import Flora.Model.Requirement import Flora.Search (SearchAction (..)) +import FloraWeb.Components.Icons import FloraWeb.Components.PackageListItem (licenseIcon, packageListItem, requirementListItem) import FloraWeb.Components.PaginationNav (paginationNav) -import FloraWeb.Components.Utils (text) +import FloraWeb.Components.SlimSearchBar +import FloraWeb.Components.Utils import FloraWeb.Links qualified as Links import FloraWeb.Pages.Templates (FloraHTML, TemplateEnv (..)) import FloraWeb.Pages.Templates.Haddock (renderHaddock) @@ -101,19 +101,28 @@ showDependents -> Word -> Vector DependencyInfo -> Positive Word + -> Maybe Text -> FloraHTML -showDependents namespace packageName release count packagesInfo currentPage = +showDependents namespace packageName release count packagesInfo currentPage mSearch = div_ [class_ "container"] $ do presentationHeaderForSubpage namespace packageName release Dependents count - div_ [class_ ""] $ do - ul_ [class_ "package-list"] $ - Vector.forM_ - packagesInfo - ( \dep -> - packageListItem (dep.namespace, dep.name, dep.latestSynopsis, dep.latestVersion, dep.latestLicense) - ) - when (count > 30) $ - paginationNav count currentPage (DependentsOf namespace packageName) + let placeholder = fromMaybe "Search dependents" mSearch + let value = fromMaybe "" mSearch + ul_ [class_ "package-list"] $ do + slimSearchBar (SearchBarOptions{actionUrl = "", placeholder, value}) + Vector.forM_ + packagesInfo + ( \dep -> + packageListItem + ( dep.namespace + , dep.name + , dep.latestSynopsis + , dep.latestVersion + , dep.latestLicense + ) + ) + when (count > 30) $ + paginationNav count currentPage (DependentsOf namespace packageName Nothing) showDependencies :: Namespace -> PackageName -> Release -> ComponentDependencies -> FloraHTML showDependencies namespace packageName release componentsInfo = do @@ -126,12 +135,11 @@ listVersions :: Namespace -> PackageName -> Vector Release -> FloraHTML listVersions namespace packageName releases = div_ [class_ "container"] $ do presentationHeaderForVersions namespace packageName (fromIntegral $ Vector.length releases) - div_ [class_ ""] $ - ul_ [class_ "package-list"] $ - Vector.forM_ - releases - ( \release -> versionListItem namespace packageName release - ) + ul_ [class_ "package-list"] $ + Vector.forM_ + releases + ( \release -> versionListItem namespace packageName release + ) versionListItem :: Namespace -> PackageName -> Release -> FloraHTML versionListItem namespace packageName release = do @@ -152,9 +160,18 @@ versionListItem namespace packageName release = do toHtml release.license -- | Render a list of package informations -packageListing :: Vector PackageInfo -> FloraHTML -packageListing packages = - ul_ [class_ "package-list"] $ +packageListing + :: Maybe (Vector PackageInfo) + -- ^ Priority items that are highlighted, + -- like exact matches for a search + -> Vector PackageInfo + -> FloraHTML +packageListing mExactMatchItems packages = + ul_ [class_ "package-list"] $ do + whenJust mExactMatchItems $ \exactMatchItems -> + forM_ exactMatchItems $ \em -> do + div_ [class_ "exact-match"] $ + packageListItem (em.namespace, em.name, em.synopsis, em.version, em.license) Vector.forM_ packages ( \PackageInfo{..} -> packageListItem (namespace, name, synopsis, version, license) @@ -270,8 +287,7 @@ displayVersions namespace packageName versions numberOfReleases = span_ [] $ do toHtml $ Time.formatTime defaultTimeLocale "%a, %_d %b %Y" ts case release.revisedAt of - Nothing -> do - span_ [] "" + Nothing -> span_ [] "" Just revisionDate -> do span_ [ dataText_ @@ -299,7 +315,7 @@ displayDependencies (namespace, packageName, version) numberOfDependencies depen showAll :: Target -> Maybe Version -> Namespace -> PackageName -> FloraHTML showAll target mVersion namespace packageName = do let resource = case target of - Dependents -> Links.packageDependents namespace packageName (PositiveUnsafe 1) + Dependents -> Links.packageDependents namespace packageName (PositiveUnsafe 1) Nothing Dependencies -> Links.packageDependencies namespace packageName (fromJust mVersion) Versions -> Links.packageVersions namespace packageName a_ [class_ "dependency", href_ ("/" <> toUrlPiece resource)] "Show all…" @@ -454,35 +470,6 @@ defaultMarker :: Bool -> FloraHTML defaultMarker True = em_ "(on by default)" defaultMarker False = em_ "(off by default)" ---- - -usageInstructionTooltip :: FloraHTML -usageInstructionTooltip = - toHtmlRaw @Text - [str| - - - -|] - -chevronRightOutline :: FloraHTML -chevronRightOutline = - toHtmlRaw @Text - [str| - - - -|] - -pen :: FloraHTML -pen = - toHtmlRaw @Text - [str| - - - -|] - -- | @datalist@ element dataText_ :: Text -> Attribute dataText_ = makeAttribute "data-text" diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Categories/Show.hs b/src/web/FloraWeb/Pages/Templates/Pages/Categories/Show.hs index 3347bc6e..4269f9d2 100644 --- a/src/web/FloraWeb/Pages/Templates/Pages/Categories/Show.hs +++ b/src/web/FloraWeb/Pages/Templates/Pages/Categories/Show.hs @@ -14,4 +14,4 @@ showCategory :: Category -> Vector PackageInfo -> FloraHTML showCategory Category{name, synopsis} packagesInfo = do div_ [class_ "container"] $ do presentationHeader name synopsis (fromIntegral $ V.length packagesInfo) - packageListing packagesInfo + packageListing Nothing packagesInfo diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Home.hs b/src/web/FloraWeb/Pages/Templates/Pages/Home.hs index d8390549..358f58f4 100644 --- a/src/web/FloraWeb/Pages/Templates/Pages/Home.hs +++ b/src/web/FloraWeb/Pages/Templates/Pages/Home.hs @@ -6,26 +6,17 @@ import CMarkGFM import Control.Monad.Reader import Data.Text (Text) import Lucid -import Lucid.Svg - ( d_ - , fill_ - , path_ - , stroke_ - , stroke_linecap_ - , stroke_linejoin_ - , stroke_width_ - , viewBox_ - ) import PyF import Flora.Environment +import FloraWeb.Components.MainSearchBar (mainSearchBar) import FloraWeb.Pages.Templates.Types show :: FloraHTML show = do banner div_ [class_ "container-small"] $ do - searchBar + mainSearchBar buttons banner :: FloraHTML @@ -34,26 +25,6 @@ banner = do h1_ [class_ "main-title"] $ span_ [class_ "main-title"] "Search Haskell packages on Flora" -searchBar :: FloraHTML -searchBar = - form_ [action_ "/search", method_ "GET"] $ do - div_ [class_ "main-search"] $ do - label_ [for_ "search"] "" - input_ - [ class_ - "search-bar" - , type_ "search" - , id_ "search" - , name_ "q" - , placeholder_ "Find a package" - , value_ "" - , tabindex_ "1" - , autofocus_ - ] - button_ [type_ "submit"] $ - svg_ [xmlns_ "http://www.w3.org/2000/svg", style_ "color: gray", fill_ "none", viewBox_ "0 0 24 24", stroke_ "currentColor"] $ - path_ [stroke_linecap_ "round", stroke_linejoin_ "round", stroke_width_ "2", d_ "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"] - buttons :: FloraHTML buttons = section_ [id_ "main-page-buttons"] $ do diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs b/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs index 2225bf55..7757a16a 100644 --- a/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs +++ b/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs @@ -13,9 +13,9 @@ import Lucid.Orphans () import Flora.Model.Category.Types (Category (..)) import Flora.Model.Package.Types import Flora.Model.Release.Types (Release (..)) +import FloraWeb.Components.Icons import FloraWeb.Pages.Templates.Packages - ( chevronRightOutline - , displayCategories + ( displayCategories , displayDependencies , displayDependents , displayInstructions diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Search.hs b/src/web/FloraWeb/Pages/Templates/Pages/Search.hs index c079e2ed..13f998b6 100644 --- a/src/web/FloraWeb/Pages/Templates/Pages/Search.hs +++ b/src/web/FloraWeb/Pages/Templates/Pages/Search.hs @@ -1,7 +1,6 @@ module FloraWeb.Pages.Templates.Pages.Search where import Control.Monad (when) -import Data.Foldable (forM_) import Data.Positive import Data.Text (Text) import Data.Text.Display (display) @@ -11,7 +10,6 @@ import Lucid import Flora.Model.Package (Namespace, PackageInfo (..)) import Flora.Search (SearchAction (..)) import FloraWeb.Components.PackageListHeader (presentationHeader) -import FloraWeb.Components.PackageListItem import FloraWeb.Components.PaginationNav (paginationNav) import FloraWeb.Pages.Templates import FloraWeb.Pages.Templates.Packages (packageListing) @@ -20,14 +18,14 @@ showAllPackages :: Word -> Positive Word -> Vector PackageInfo -> FloraHTML showAllPackages count currentPage packagesInfo = do div_ [class_ "container"] $ do presentationHeader "Packages" "" count - div_ [class_ ""] $ packageListing packagesInfo + div_ [class_ ""] $ packageListing Nothing packagesInfo paginationNav count currentPage ListAllPackages showAllPackagesInNamespace :: Namespace -> Word -> Positive Word -> Vector PackageInfo -> FloraHTML showAllPackagesInNamespace namespace count currentPage packagesInfo = do div_ [class_ "container"] $ do presentationHeader (display namespace) "" count - div_ [class_ ""] $ packageListing packagesInfo + div_ [class_ ""] $ packageListing Nothing packagesInfo paginationNav count currentPage (ListAllPackagesInNamespace namespace) showResults @@ -42,9 +40,6 @@ showResults showResults searchString count currentPage exactMatches results = do div_ [class_ "container"] $ do presentationHeader searchString "" count - forM_ exactMatches $ \em -> do - div_ [class_ "exact-match"] $ - packageListItem (em.namespace, em.name, em.synopsis, em.version, em.license) - div_ [class_ ""] $ packageListing results + packageListing (Just exactMatches) results when (count > 30) $ paginationNav count currentPage (SearchPackages searchString) diff --git a/test/Flora/PackageSpec.hs b/test/Flora/PackageSpec.hs index 649d5254..d5912cfc 100644 --- a/test/Flora/PackageSpec.hs +++ b/test/Flora/PackageSpec.hs @@ -135,7 +135,7 @@ testCorrectNumberInHaskellNamespace = do testBytestringDependents :: TestEff () testBytestringDependents = do - results <- Query.getAllPackageDependentsWithLatestVersion (Namespace "haskell") (PackageName "bytestring") (0, 30) + results <- Query.getAllPackageDependentsWithLatestVersion (Namespace "haskell") (PackageName "bytestring") (0, 30) Nothing assertEqual 24 (Vector.length results) From 6cad303cedebeef58b1d7829fc8f6ae36bd98623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Wed, 1 Nov 2023 18:38:47 +0100 Subject: [PATCH 23/40] [NO-ISSUE] Fix CHANGELOG entry for #440 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71a50e9a..454f9dda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 1.0.14 -- XXXX-XX-XX * Colourise in red deprecation markers on the package page ([#438](/~https://github.com/flora-pm/flora-server/pull/439)) -* Added more matches to the the natural language processing catergory ([#44](/~https://github.com/flora-pm/flora-server/pull/440)) +* Added more matches to the the natural language processing catergory ([#440](/~https://github.com/flora-pm/flora-server/pull/440)) * Allow package imports from multiple repositories ([#444](/~https://github.com/flora-pm/flora-server/pull/444)) * Add a page on namespaces in the documentation ([#451](/~https://github.com/flora-pm/flora-server/pull/451)) * Add initial support for hosting package tarballs ([#452](/~https://github.com/flora-pm/flora-server/pull/452)) From 0debc9ea74f17ffd84e083c853ff934087889c07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Wed, 1 Nov 2023 19:24:12 +0100 Subject: [PATCH 24/40] [NO-ISSUE] Fix the revision svg icon's size --- assets/css/3-screens/1-package/1-package.css | 11 +++++++++++ test/Flora/PackageSpec.hs | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/assets/css/3-screens/1-package/1-package.css b/assets/css/3-screens/1-package/1-package.css index 71bf741d..0bda37af 100644 --- a/assets/css/3-screens/1-package/1-package.css +++ b/assets/css/3-screens/1-package/1-package.css @@ -152,6 +152,17 @@ span.revised-date:hover::before { display: block; } +.revised-date { + svg { + display: inline; + width: 1rem; + height: 1rem; + } + + position: relative; /* making the .tooltip span a container for the tooltip text */ + border-bottom: 1px dashed #000; /* little indicater to indicate it's hoverable */ +} + .instruction-tooltip { svg { display: inline; diff --git a/test/Flora/PackageSpec.hs b/test/Flora/PackageSpec.hs index d5912cfc..4ef929fc 100644 --- a/test/Flora/PackageSpec.hs +++ b/test/Flora/PackageSpec.hs @@ -137,7 +137,7 @@ testBytestringDependents :: TestEff () testBytestringDependents = do results <- Query.getAllPackageDependentsWithLatestVersion (Namespace "haskell") (PackageName "bytestring") (0, 30) Nothing assertEqual - 24 + 23 (Vector.length results) testNoSelfDependent :: TestEff () From fb4ec8e74bd620d966ee91d2cbe233630eceffa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Mon, 13 Nov 2023 00:41:00 +0100 Subject: [PATCH 25/40] remove euo pipefail from scripts/.zshrc --- scripts/.zshrc | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/.zshrc b/scripts/.zshrc index 02ecec4d..9942263a 100644 --- a/scripts/.zshrc +++ b/scripts/.zshrc @@ -1,7 +1,5 @@ #!/usr/bin/env zsh -set -euo pipefail - export SHELL="zsh" export ZSH="$HOME/.oh-my-zsh" export LANG=C.UTF-8 From 5c999e2ede4a38f6213088686aecf53613db0ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Mon, 20 Nov 2023 00:22:42 +0100 Subject: [PATCH 26/40] [NO-ISSUE] Add GHC-9.4.8 in the list of known versions (#481) --- src/core/Flora/Import/Package.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/Flora/Import/Package.hs b/src/core/Flora/Import/Package.hs index d76d86fe..81cc1f9a 100644 --- a/src/core/Flora/Import/Package.hs +++ b/src/core/Flora/Import/Package.hs @@ -129,6 +129,7 @@ versionList = , Version.mkVersion [9, 6, 3] , Version.mkVersion [9, 6, 2] , Version.mkVersion [9, 6, 1] + , Version.mkVersion [9, 4, 8] , Version.mkVersion [9, 4, 7] , Version.mkVersion [9, 4, 6] , Version.mkVersion [9, 4, 5] From e1ad53950006256a52d295732b8c32ce6f9e7bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Tue, 21 Nov 2023 10:28:55 +0100 Subject: [PATCH 27/40] [NO-ISSUE] Fix non-existent docs directory (#480) --- .github/workflows/docker-image.yml | 6 +----- .github/workflows/test-docker-image.yml | 17 +++++++++++++++++ Dockerfile | 4 ++-- 3 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/test-docker-image.yml diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 6c68ed44..0f0eb1b5 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -1,12 +1,8 @@ name: Publish Docker Image on: - pull_request: - paths: - - Dockerfile - - docker-compose.yml - - scripts/.zshrc push: + branches: ["development"] paths: - Dockerfile - docker-compose.yml diff --git a/.github/workflows/test-docker-image.yml b/.github/workflows/test-docker-image.yml new file mode 100644 index 00000000..bc31d652 --- /dev/null +++ b/.github/workflows/test-docker-image.yml @@ -0,0 +1,17 @@ +name: Test Docker Image + +on: + pull_request: + branches: ["main", "development"] + paths: + - Dockerfile + - docker-compose.yml + - scripts/.zshrc + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build the hello-docker Docker image + run: make docker-build diff --git a/Dockerfile b/Dockerfile index 40884c14..ca49a0c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ ARG GID=1000 ARG UID=1000 ARG ghc_version=9.4.5 -ARG cabal_version=3.10.1.0 +ARG cabal_version=3.10.2.0 # generate a working directory USER "root" @@ -41,7 +41,6 @@ RUN chmod ugo+x /home/$USER/.cabal USER ${USER} RUN git config --global --add safe.directory "*" -RUN ls -lh /home/$USER/.cabal RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh @@ -84,6 +83,7 @@ RUN make souffle # copy and build the assets COPY --chown=${USER} assets ./assets +COPY --chown=${USER} docs ./docs RUN make build-assets USER root From a5e7146edfd57214b53fb12c5c71c9d6b9b6d5ba Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Sun, 26 Nov 2023 21:30:26 +0000 Subject: [PATCH 28/40] [FLORA-443] support non hackage repo urls (#479) * [FLORA-443] support non hackage repo urls * Added line in CHANGELOG.md --- CHANGELOG.md | 1 + environment.docker.sh | 2 + src/core/Flora/Model/Package/Types.hs | 7 ++ src/core/Flora/Model/PackageIndex/Types.hs | 2 +- src/core/Flora/Model/PackageIndex/Update.hs | 3 + src/web/FloraWeb/Common/Guards.hs | 14 +++ src/web/FloraWeb/Pages/Server/Packages.hs | 5 + src/web/FloraWeb/Pages/Templates/Packages.hs | 108 +++++++++--------- .../Pages/Templates/Pages/Packages.hs | 7 +- 9 files changed, 96 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 454f9dda..36ee64a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Add initial support for hosting package tarballs ([#452](/~https://github.com/flora-pm/flora-server/pull/452)) * Show depended on components in dependencies page ([#464](/~https://github.com/flora-pm/flora-server/pull/464)) * Add search bar for reverse dependencies ([#476](/~https://github.com/flora-pm/flora-server/pull/476)) +* Support non Hackage repo URLs ([#479](/~https://github.com/flora-pm/flora-server/pull/479)) ## 1.0.13 -- 2023-09-17 * Exclude deprecated releases from latest versions and search ([#373](/~https://github.com/flora-pm/flora-server/pull/373)) diff --git a/environment.docker.sh b/environment.docker.sh index d2dc616a..90c5e1bb 100755 --- a/environment.docker.sh +++ b/environment.docker.sh @@ -6,3 +6,5 @@ export FLORA_HTTP_PORT=8084 export FLORA_DB_CONNSTRING="host=${FLORA_DB_HOST} dbname=${FLORA_DB_DATABASE}\ user=${FLORA_DB_USER} password=${FLORA_DB_PASSWORD}" export PGPASSWORD=${FLORA_DB_PASSWORD} + +export FLORA_PG_URI="postgresql://${FLORA_DB_USER}:${FLORA_DB_PASSWORD}@${FLORA_DB_HOST}:${FLORA_DB_PORT}/${FLORA_DB_DATABASE}" diff --git a/src/core/Flora/Model/Package/Types.hs b/src/core/Flora/Model/Package/Types.hs index de3a039f..3d9ecb21 100644 --- a/src/core/Flora/Model/Package/Types.hs +++ b/src/core/Flora/Model/Package/Types.hs @@ -80,6 +80,9 @@ instance FromHttpApiData PackageName where Nothing -> Left "Could not parse package name" Just a -> Right a +extractPackageNameText :: PackageName -> Text +extractPackageNameText (PackageName text) = text + parsePackageName :: Text -> Maybe PackageName parsePackageName txt = if matches "[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*" txt @@ -149,6 +152,10 @@ parseNamespace txt = then Just $ Namespace txt else Nothing +extractNamespaceText :: Namespace -> Text +extractNamespaceText (Namespace text) = + fromMaybe text (Text.stripPrefix "@" text) + instance ToSchema Namespace where declareNamedSchema proxy = genericDeclareNamedSchema openApiSchemaOptions proxy diff --git a/src/core/Flora/Model/PackageIndex/Types.hs b/src/core/Flora/Model/PackageIndex/Types.hs index 20f0f6c8..2053abc4 100644 --- a/src/core/Flora/Model/PackageIndex/Types.hs +++ b/src/core/Flora/Model/PackageIndex/Types.hs @@ -25,8 +25,8 @@ newtype PackageIndexId = PackageIndexId {getPackageIndexId :: UUID} data PackageIndex = PackageIndex { packageIndexId :: PackageIndexId , repository :: Text - , url :: Text , timestamp :: Maybe UTCTime + , url :: Text } deriving stock (Eq, Show, Generic) deriving anyclass (FromRow, ToRow, NFData) diff --git a/src/core/Flora/Model/PackageIndex/Update.hs b/src/core/Flora/Model/PackageIndex/Update.hs index 4d63389d..bd188c7e 100644 --- a/src/core/Flora/Model/PackageIndex/Update.hs +++ b/src/core/Flora/Model/PackageIndex/Update.hs @@ -16,6 +16,9 @@ import Effectful import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff) import Flora.Model.PackageIndex.Types + ( PackageIndex + , mkPackageIndex + ) updatePackageIndexByName :: DB :> es => Text -> Maybe UTCTime -> Eff es () updatePackageIndexByName repositoryName newTimestamp = do diff --git a/src/web/FloraWeb/Common/Guards.hs b/src/web/FloraWeb/Common/Guards.hs index 7198135a..5ca15720 100644 --- a/src/web/FloraWeb/Common/Guards.hs +++ b/src/web/FloraWeb/Common/Guards.hs @@ -9,6 +9,8 @@ import Effectful.PostgreSQL.Transact.Effect import Effectful.Time (Time) import Flora.Model.Package import Flora.Model.Package.Query qualified as Query +import Flora.Model.PackageIndex.Query as Query +import Flora.Model.PackageIndex.Types (PackageIndex) import Flora.Model.Release.Query qualified as Query import Flora.Model.Release.Types (Release) @@ -40,3 +42,15 @@ guardThatReleaseExists packageId version action = do case result of Just release -> pure release Nothing -> action version + +guardThatPackageIndexExists + :: DB :> es + => Namespace + -> (Namespace -> Eff es PackageIndex) + -- ^ Action to run if the package index does not exist + -> Eff es PackageIndex +guardThatPackageIndexExists namespace action = do + result <- Query.getPackageIndexByName (extractNamespaceText namespace) + case result of + Just packageIndex -> pure packageIndex + Nothing -> action namespace diff --git a/src/web/FloraWeb/Pages/Server/Packages.hs b/src/web/FloraWeb/Pages/Server/Packages.hs index 3316b6d6..259d436c 100644 --- a/src/web/FloraWeb/Pages/Server/Packages.hs +++ b/src/web/FloraWeb/Pages/Server/Packages.hs @@ -30,6 +30,7 @@ import Flora.Logging import Flora.Model.BlobIndex.Query qualified as Query import Flora.Model.Package import Flora.Model.Package.Query qualified as Query +import Flora.Model.PackageIndex.Types (PackageIndex (..)) import Flora.Model.Release.Query qualified as Query import Flora.Model.Release.Types import Flora.Search qualified as Search @@ -90,6 +91,7 @@ showPackageVersion namespace packageName mversion = do session <- getSession templateEnv' <- fromSession session defaultTemplateEnv package <- guardThatPackageExists namespace packageName (\_ _ -> web404) + packageIndex <- guardThatPackageIndexExists namespace $ const web404 releases <- Query.getReleases package.packageId let latestRelease = releases @@ -129,12 +131,15 @@ showPackageVersion namespace packageName mversion = do , "package" .= (display namespace <> "/" <> display packageName) ] + let packageIndexURL = packageIndex.url + render templateEnv $ Packages.showPackage release releases numberOfReleases package + packageIndexURL dependents numberOfDependents releaseDependencies diff --git a/src/web/FloraWeb/Pages/Templates/Packages.hs b/src/web/FloraWeb/Pages/Templates/Packages.hs index 842c05de..b170f273 100644 --- a/src/web/FloraWeb/Pages/Templates/Packages.hs +++ b/src/web/FloraWeb/Pages/Templates/Packages.hs @@ -62,18 +62,18 @@ presentationHeaderForSubpage -> Word -> FloraHTML presentationHeaderForSubpage namespace packageName release target numberOfPackages = div_ [class_ "divider"] $ do - div_ [class_ "page-title"] $ do - h1_ [class_ ""] $ do - span_ [class_ "headline"] $ do - displayNamespace namespace - chevronRightOutline - linkToPackageWithVersion namespace packageName (release.version) - chevronRightOutline - toHtml (display target) + div_ [class_ "page-title"] $ h1_ [class_ ""] $ do + span_ [class_ "headline"] $ do + displayNamespace namespace + chevronRightOutline + linkToPackageWithVersion namespace packageName (release.version) + chevronRightOutline + toHtml (display target) p_ [class_ "synopsis"] $ span_ [class_ "version"] $ toHtml $ - display numberOfPackages <> " results" + display numberOfPackages + <> " results" presentationHeaderForVersions :: Namespace @@ -81,18 +81,18 @@ presentationHeaderForVersions -> Word -> FloraHTML presentationHeaderForVersions namespace packageName numberOfReleases = div_ [class_ "divider"] $ do - div_ [class_ "page-title"] $ do - h1_ [class_ ""] $ do - span_ [class_ "headline"] $ do - displayNamespace namespace - chevronRightOutline - linkToPackage namespace packageName - chevronRightOutline - toHtml (display Versions) + div_ [class_ "page-title"] $ h1_ [class_ ""] $ do + span_ [class_ "headline"] $ do + displayNamespace namespace + chevronRightOutline + linkToPackage namespace packageName + chevronRightOutline + toHtml (display Versions) p_ [class_ "synopsis"] $ span_ [class_ "version"] $ toHtml $ - display numberOfReleases <> " results" + display numberOfReleases + <> " results" showDependents :: Namespace @@ -138,7 +138,7 @@ listVersions namespace packageName releases = ul_ [class_ "package-list"] $ Vector.forM_ releases - ( \release -> versionListItem namespace packageName release + ( versionListItem namespace packageName ) versionListItem :: Namespace -> PackageName -> Release -> FloraHTML @@ -149,15 +149,19 @@ versionListItem namespace packageName release = do Just ts -> span_ [class_ "package-list-item__synopsis"] (toHtml $ Time.formatTime defaultTimeLocale "%a, %_d %b %Y" ts) li_ [class_ "package-list-item"] $ - a_ [href, class_ ""] $ do - h4_ [class_ "package-list-item__name"] $ - strong_ [class_ ""] . toHtml $ - "v" <> toHtml release.version - uploadedAt - div_ [class_ "package-list-item__metadata"] $ - span_ [class_ "package-list-item__license"] $ do - licenseIcon - toHtml release.license + a_ [href, class_ ""] $ + do + h4_ [class_ "package-list-item__name"] + $ strong_ [class_ ""] + . toHtml + $ "v" + <> toHtml release.version + uploadedAt + div_ [class_ "package-list-item__metadata"] $ + span_ [class_ "package-list-item__license"] $ + do + licenseIcon + toHtml release.license -- | Render a list of package informations packageListing @@ -169,7 +173,7 @@ packageListing packageListing mExactMatchItems packages = ul_ [class_ "package-list"] $ do whenJust mExactMatchItems $ \exactMatchItems -> - forM_ exactMatchItems $ \em -> do + forM_ exactMatchItems $ \em -> div_ [class_ "exact-match"] $ packageListItem (em.namespace, em.name, em.synopsis, em.version, em.license) Vector.forM_ @@ -182,17 +186,17 @@ requirementListing requirements = ul_ [class_ "component-list"] $ requirementListItem requirements showChangelog :: Namespace -> PackageName -> Version -> Maybe TextHtml -> FloraHTML -showChangelog namespace packageName version mChangelog = div_ [class_ "container"] $ do - div_ [class_ "divider"] $ do - div_ [class_ "page-title"] $ - h1_ [class_ ""] $ do +showChangelog namespace packageName version mChangelog = div_ [class_ "container"] $ div_ [class_ "divider"] $ do + div_ [class_ "page-title"] $ + h1_ [class_ ""] $ + do span_ [class_ "headline"] $ toHtml ("Changelog of " <> display namespace <> "/" <> display packageName) toHtmlRaw @Text " " span_ [class_ "version"] $ toHtml $ display version - section_ [class_ "release-changelog"] $ do - case mChangelog of - Nothing -> toHtml @Text "This release does not have a Changelog" - Just (MkTextHtml changelogText) -> relaxHtmlT changelogText + section_ [class_ "release-changelog"] $ do + case mChangelog of + Nothing -> toHtml @Text "This release does not have a Changelog" + Just (MkTextHtml changelogText) -> relaxHtmlT changelogText displayReleaseVersion :: Version -> FloraHTML displayReleaseVersion = toHtml @@ -237,13 +241,14 @@ displayCategories categories = div_ [class_ "license "] $ h3_ [class_ "package-body-section"] "Categories" ul_ [class_ "categories"] $ foldMap renderCategory categories -displayLinks :: Namespace -> PackageName -> Release -> FloraHTML -displayLinks namespace packageName release = +displayLinks :: Namespace -> PackageName -> Text -> Release -> FloraHTML +displayLinks namespace packageName packageIndexURL release = li_ [class_ ""] $ do h3_ [class_ "package-body-section links"] "Links" ul_ [class_ "links"] $ do li_ [class_ "package-link"] $ a_ [href_ (getHomepage release)] "Homepage" - li_ [class_ "package-link"] $ a_ [href_ ("https://hackage.haskell.org/package/" <> display packageName <> "-" <> display release.version)] "Documentation" + li_ [class_ "package-link"] $ a_ [href_ (packageIndexURL <> "/package/" <> display packageName <> "-" <> display release.version)] "Documentation" + li_ [class_ "package-link"] $ displaySourceRepos release.sourceRepos li_ [class_ "package-link"] $ displayChangelog namespace packageName release.version release.changelog @@ -288,7 +293,7 @@ displayVersions namespace packageName versions numberOfReleases = toHtml $ Time.formatTime defaultTimeLocale "%a, %_d %b %Y" ts case release.revisedAt of Nothing -> span_ [] "" - Just revisionDate -> do + Just revisionDate -> span_ [ dataText_ ("Revised on " <> display (Time.formatTime defaultTimeLocale "%a, %_d %b %Y, %R %EZ" revisionDate)) @@ -334,7 +339,7 @@ displayInstructions namespace packageName latestRelease = , readonly_ "readonly" ] TemplateEnv{features} <- ask - when (isJust $ features.blobStoreImpl) $ do + when (isJust features.blobStoreImpl) $ do label_ [for_ "tarball", class_ "font-light"] "Download" let v = display latestRelease.version tarballName = display packageName <> "-" <> v <> ".tar.gz" @@ -352,11 +357,12 @@ displayPackageDeprecation (PackageAlternatives inFavourOf) = else do label_ [for_ "install-string", class_ "font-light"] "This package has been deprecated in favour of" ul_ [class_ "package-alternatives"] $ - Vector.forM_ inFavourOf $ \PackageAlternative{namespace, package} -> - li_ [] $ - a_ - [href_ ("/packages/" <> display namespace <> "/" <> display package)] - (text $ display namespace <> "/" <> display package) + Vector.forM_ inFavourOf $ + \PackageAlternative{namespace, package} -> + li_ [] $ + a_ + [href_ ("/packages/" <> display namespace <> "/" <> display package)] + (text $ display namespace <> "/" <> display package) displayReleaseDeprecation :: Maybe (Namespace, PackageName, Version) -> FloraHTML displayReleaseDeprecation mLatestViableRelease = @@ -463,8 +469,7 @@ displayPackageFlag MkPackageFlag{flagName, flagDescription, flagDefault} = case pre_ [class_ "package-flag-name"] (toHtml $ Text.pack (Flag.unFlagName flagName)) toHtmlRaw @Text " " defaultMarker flagDefault - div_ [class_ "package-flag-description"] $ do - renderHaddock $ Text.pack flagDescription + div_ [class_ "package-flag-description"] $ renderHaddock $ Text.pack flagDescription defaultMarker :: Bool -> FloraHTML defaultMarker True = em_ "(on by default)" @@ -482,8 +487,9 @@ intercalateVec sep vector = formatInstallString :: PackageName -> Release -> Text formatInstallString packageName Release{version} = - Text.pack . render $ - hcat [pretty packageName, PP.space, rangedVersion, ","] + Text.pack + . render + $ hcat [pretty packageName, PP.space, rangedVersion, ","] where rangedVersion :: Doc rangedVersion = "^>=" <> majMin diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs b/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs index 7757a16a..8fd93a65 100644 --- a/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs +++ b/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs @@ -38,6 +38,7 @@ showPackage -> Vector Release -> Word -> Package + -> Text -> Vector Package -> Word -> Vector (Namespace, PackageName, Text) @@ -49,6 +50,7 @@ showPackage packageReleases numberOfReleases package@Package{namespace, name} + packageIndexURL dependents numberOfDependents dependencies @@ -58,6 +60,7 @@ showPackage presentationHeader latestRelease namespace name latestRelease.synopsis packageBody package + packageIndexURL latestRelease packageReleases numberOfReleases @@ -83,6 +86,7 @@ presentationHeader release namespace name synopsis = packageBody :: Package + -> Text -> Release -> Vector Release -> Word @@ -94,6 +98,7 @@ packageBody -> FloraHTML packageBody Package{namespace, name = packageName, deprecationInfo} + packageIndexURL latestRelease@Release{flags, deprecated, license, maintainer, version} packageReleases numberOfReleases @@ -107,7 +112,7 @@ packageBody displayCategories categories displayLicense license displayMaintainer maintainer - displayLinks namespace packageName latestRelease + displayLinks namespace packageName packageIndexURL latestRelease displayVersions namespace packageName packageReleases numberOfReleases div_ [class_ "release-readme-column"] $ div_ [class_ "release-readme"] $ displayReadme latestRelease div_ [class_ "package-right-column"] $ ul_ [class_ "package-right-rows"] $ do From 94832900c6a17c4f3d0989e336fe1ef8dd89370c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Wed, 29 Nov 2023 11:21:16 +0100 Subject: [PATCH 29/40] [NO-ISSUE] Handle "@haskell" as "@hackage" for package indexes --- flora.cabal | 2 +- src/core/Flora/Model/PackageIndex/Query.hs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/flora.cabal b/flora.cabal index fed71ac6..1e4e4b92 100644 --- a/flora.cabal +++ b/flora.cabal @@ -27,13 +27,13 @@ flag prod common common-extensions default-extensions: - NoStarIsType DataKinds DeriveAnyClass DerivingStrategies DerivingVia DuplicateRecordFields LambdaCase + NoStarIsType OverloadedLabels OverloadedRecordDot OverloadedStrings diff --git a/src/core/Flora/Model/PackageIndex/Query.hs b/src/core/Flora/Model/PackageIndex/Query.hs index 74a12585..e8d34fd4 100644 --- a/src/core/Flora/Model/PackageIndex/Query.hs +++ b/src/core/Flora/Model/PackageIndex/Query.hs @@ -14,5 +14,8 @@ import Flora.Model.PackageIndex.Types getPackageIndexByName :: DB :> es => Text -> Eff es (Maybe PackageIndex) getPackageIndexByName repository = - dbtToEff $ - selectOneByField [field| repository |] (Only repository) + let index = case repository of + "haskell" -> "hackage" + r -> r + in dbtToEff $ + selectOneByField [field| repository |] (Only index) From a4dc4d1c3b729ee4134779c03cf10cfdd2718d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sun, 3 Dec 2023 13:29:51 +0100 Subject: [PATCH 30/40] [NO-ISSUE] Improve the README and contributing guide --- CONTRIBUTING.md | 31 ++++++++++++++++++++++++------- README.md | 24 ++++++++++++++---------- 2 files changed, 38 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 141d13ec..6bbeee44 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -181,18 +181,35 @@ $ source environment.docker.sh # You'll be in a tmux session, everything should be launched # Visit localhost:8084 from your web browser to see if it all works. ``` +### Provisioning the database -To provision the development database, type: +After everything is set up, (locally or via Docker), you can start populating the database: ```bash -$ make docker-enter -(docker)$ source environment.docker.sh -(docker)$ make db-drop # password is 'postgres' by default -(docker)$ make db-setup # password is 'postgres' by default -(docker)$ make db-provision -# And you should be good! +$ make db-setup +$ make db-provision +$ cabal run -- flora-cli create-user --admin --can-login --username "admin" \ + --email "admin@localhost" --password "password123" +$ make db-provision-test-packages ``` +### Importing a package index + +The previous paragraph shows how to import test packages, but you may want to import a whole package index, for shit and giggles. + +You can do so with: + +```bash +$ cabal run flora-cli -- import-index ~/.cabal/packages/hackage.haskell.org/01-index.tar.gz \ + --repository hackage.haskell.org +``` + +Similarly if you have the [cardano packages index](https://input-output-hk.github.io/cardano-haskell-packages/) configured, run: + +```bash +$ cabal run flora-cli -- import-index ~/.cabal/packages/cardano/01-index.tar.gz \ + --repository "cardano" +``` ### Nix diff --git a/README.md b/README.md index a0b096f0..4b220740 100644 --- a/README.md +++ b/README.md @@ -42,18 +42,22 @@

-**Read More** +## ⚡ Features -* [Code of Conduct](./CODE_OF_CONDUCT.md) -* [Contribution Guide](./CONTRIBUTING.md) -* [Development Wiki](/~https://github.com/flora-pm/flora-server/wiki) +* 📁 Curated category model, with elimination of duplicates +* 🏛️ Package namespaces, so that packages with the same name can live without conflict +* 🌓 Dark and light modes +* 📱 Mobile user interface -### Importing everything from Hackage +## 🤝 Contributing -1. Download the archive containing all packages [here](https://hackage.haskell.org/01-index.tar) -2. Extract it in Flora's root directory. You should now have a `01-index` folder -3. Run `make import-from-hackage` +We welcome new contributors! Join the [Matrix chatroom](https://app.element.io/#/room/#flora-pm:matrix.org) or open a [Discussion](/~https://github.com/flora-pm/flora-server/discussions/new/choose). ---- +To setup a local installation, see [CONTRIBUTING.md#project-setup](/~https://github.com/flora-pm/flora-server/blob/development/CONTRIBUTING.md#project-setup) + +## 📖 Read More -You can explore the Makefile rules by typing `make` in your shell. I promise you it's worth it. +* [Code of Conduct](./CODE_OF_CONDUCT.md) +* [Development Wiki](/~https://github.com/flora-pm/flora-server/wiki) + +--- From f45fc39ed1cea14155652908f198ec2b3d1acd65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 12:46:54 +0100 Subject: [PATCH 31/40] Bump cachix/cachix-action from 12 to 13 (#483) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nix-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nix-check.yml b/.github/workflows/nix-check.yml index 6a88119a..35f90655 100644 --- a/.github/workflows/nix-check.yml +++ b/.github/workflows/nix-check.yml @@ -11,7 +11,7 @@ jobs: - uses: cachix/install-nix-action@v23 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} - - uses: cachix/cachix-action@v12 + - uses: cachix/cachix-action@v13 with: name: flora-pm authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' From 9392b81e91b22f7a57585d35cd0f10f5b57503c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 12:47:16 +0100 Subject: [PATCH 32/40] Bump cachix/install-nix-action from 23 to 24 (#484) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/nix-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nix-check.yml b/.github/workflows/nix-check.yml index 35f90655..5b925a44 100644 --- a/.github/workflows/nix-check.yml +++ b/.github/workflows/nix-check.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: cachix/install-nix-action@v23 + - uses: cachix/install-nix-action@v24 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} - uses: cachix/cachix-action@v13 From 35d29d7681e6143dcf32b28c9ffe85ca5e6a4028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Tue, 5 Dec 2023 18:56:08 +0100 Subject: [PATCH 33/40] [FLORA-466] two factor authentication (#482) --- .github/workflows/backend.yml | 1 + CONTRIBUTING.md | 4 +- Makefile | 2 + app/cli/DesignSystem.hs | 15 +- assets/css/1-core/2-variables.css | 9 + assets/css/2-components/7-button.css | 32 + assets/css/2-components/8-alert.css | 27 + assets/css/styles.css | 10 + assets/yarn.lock | 866 +++++++++++------- cabal.project | 11 +- cabal.project.freeze | 218 +++-- design/stories/alerts.stories.js | 6 + flora.cabal | 33 +- .../20231122154627_add_totp_to_users.sql | 3 + src/core/Flora/Model/User.hs | 10 +- src/core/Flora/Model/User/Update.hs | 92 +- src/core/Flora/QRCode.hs | 20 + .../Database/PostgreSQL/Simple/Orphans.hs | 20 +- src/web/FloraWeb/Common/Auth.hs | 81 +- src/web/FloraWeb/Common/Auth/TwoFactor.hs | 44 + src/web/FloraWeb/Common/Auth/Types.hs | 41 +- src/web/FloraWeb/Common/Guards.hs | 27 + src/web/FloraWeb/Components/Alert.hs | 20 + src/web/FloraWeb/Components/Button.hs | 8 + src/web/FloraWeb/Components/Icons.hs | 20 + src/web/FloraWeb/Components/Navbar.hs | 2 +- src/web/FloraWeb/Pages/Routes.hs | 2 + src/web/FloraWeb/Pages/Routes/Sessions.hs | 2 +- src/web/FloraWeb/Pages/Routes/Settings.hs | 68 ++ src/web/FloraWeb/Pages/Server.hs | 4 +- src/web/FloraWeb/Pages/Server/Categories.hs | 2 +- src/web/FloraWeb/Pages/Server/Packages.hs | 6 +- src/web/FloraWeb/Pages/Server/Search.hs | 2 +- src/web/FloraWeb/Pages/Server/Sessions.hs | 48 +- src/web/FloraWeb/Pages/Server/Settings.hs | 130 +++ src/web/FloraWeb/Pages/Templates.hs | 10 +- src/web/FloraWeb/Pages/Templates/Packages.hs | 9 +- .../Pages/Templates/Pages/Categories.hs | 8 - .../Pages/Templates/Pages/Sessions.hs | 38 - .../Pages/Templates/Screens/Categories.hs | 8 + .../{Pages => Screens}/Categories/Index.hs | 2 +- .../{Pages => Screens}/Categories/Show.hs | 2 +- .../Templates/{Pages => Screens}/Home.hs | 2 +- .../Templates/{Pages => Screens}/Packages.hs | 2 +- .../Templates/{Pages => Screens}/Search.hs | 2 +- .../Pages/Templates/Screens/Sessions.hs | 48 + .../Pages/Templates/Screens/Settings.hs | 78 ++ src/web/FloraWeb/Pages/Templates/Types.hs | 7 +- src/web/FloraWeb/Server.hs | 91 +- test/Flora/TestUtils.hs | 4 + 50 files changed, 1598 insertions(+), 599 deletions(-) create mode 100644 assets/css/2-components/7-button.css create mode 100644 assets/css/2-components/8-alert.css create mode 100644 design/stories/alerts.stories.js create mode 100644 migrations/20231122154627_add_totp_to_users.sql create mode 100644 src/core/Flora/QRCode.hs create mode 100644 src/web/FloraWeb/Common/Auth/TwoFactor.hs create mode 100644 src/web/FloraWeb/Components/Alert.hs create mode 100644 src/web/FloraWeb/Components/Button.hs create mode 100644 src/web/FloraWeb/Pages/Routes/Settings.hs create mode 100644 src/web/FloraWeb/Pages/Server/Settings.hs delete mode 100644 src/web/FloraWeb/Pages/Templates/Pages/Categories.hs delete mode 100644 src/web/FloraWeb/Pages/Templates/Pages/Sessions.hs create mode 100644 src/web/FloraWeb/Pages/Templates/Screens/Categories.hs rename src/web/FloraWeb/Pages/Templates/{Pages => Screens}/Categories/Index.hs (89%) rename src/web/FloraWeb/Pages/Templates/{Pages => Screens}/Categories/Show.hs (90%) rename src/web/FloraWeb/Pages/Templates/{Pages => Screens}/Home.hs (97%) rename src/web/FloraWeb/Pages/Templates/{Pages => Screens}/Packages.hs (98%) rename src/web/FloraWeb/Pages/Templates/{Pages => Screens}/Search.hs (96%) create mode 100644 src/web/FloraWeb/Pages/Templates/Screens/Sessions.hs create mode 100644 src/web/FloraWeb/Pages/Templates/Screens/Settings.hs diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index 641d4a76..9c66f428 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -71,6 +71,7 @@ jobs: echo "$HOME/.cabal/bin" >> $GITHUB_PATH echo "$HOME/.local/bin" >> $GITHUB_PATH echo "$HOME/node_modules/.bin" >> $GITHUB_PATH + sudo apt install libsodium-dev source ./environment.ci.sh touch ~/.pgpass chmod 0600 ~/.pgpass diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6bbeee44..88603571 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,8 +18,8 @@ The following Haskell command-line tools will have to be installed: (Some of the above packages have incompatible dependencies, so don't try to install them all at once with `cabal install`) -You will need the [Soufflé datalog engine v2.3](/~https://github.com/souffle-lang/souffle/releases/tag/2.3) - +* [Soufflé datalog engine v2.3](/~https://github.com/souffle-lang/souffle/releases/tag/2.3): The datalog engine for package classification +* `libsodium-1.0.18`: The system library that powers most of the cryptography happening in flora * `yarn`: The tool that handles the JavaScript code bases * `esbuild`: The tool that handles asset bundling diff --git a/Makefile b/Makefile index 353d4ba0..97e01e9b 100644 --- a/Makefile +++ b/Makefile @@ -106,6 +106,8 @@ tags: ## Generate ctags for the project with `ghc-tags` design-system: ## Generate the HTML components used by the design system @cabal run -- flora-cli gen-design-system +start-design-sysytem: ## Start storybook.js + @cd design; yarn storybook help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.* ?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/app/cli/DesignSystem.hs b/app/cli/DesignSystem.hs index 46f22eca..c1c80f39 100644 --- a/app/cli/DesignSystem.hs +++ b/app/cli/DesignSystem.hs @@ -25,6 +25,7 @@ import Flora.Model.Category import Flora.Model.Category qualified as Category import Flora.Model.Package import Flora.Search +import FloraWeb.Components.Alert qualified as Component import FloraWeb.Components.CategoryCard qualified as Component import FloraWeb.Components.PackageListItem qualified as Component import FloraWeb.Components.PaginationNav qualified as Component @@ -69,6 +70,7 @@ components = ) , ("category-card", ComponentTitle "Category", ComponentName "CategoryCard", categoryCardExample) , ("pagination-area", ComponentTitle "Pagination Area", ComponentName "Pagination", paginationExample) + , ("alerts", ComponentTitle "Alerts", ComponentName "Alert", alertsExample) ] ----------------------- @@ -76,8 +78,9 @@ components = ----------------------- storyTemplate :: ComponentTitle -> ComponentName -> TL.Text -> ByteString -storyTemplate (ComponentTitle title) (ComponentName name) html = - [fmt| +storyTemplate (ComponentTitle title) (ComponentName name) unprocessedHtml = + let html = TL.replace "\n" " " unprocessedHtml + in [fmt| export default {{ title: "Components/{title}" }}; @@ -124,3 +127,11 @@ paginationExample = div_ $ do div_ $ do h4_ "Next button" Component.paginationNav 32 1 (SearchPackages "text") + +alertsExample :: FloraHTML +alertsExample = div_ $ do + div_ $ do + h4_ "Info alert" + Component.info "Info alert" + h4_ "Error alert" + Component.exception "Error alert!" diff --git a/assets/css/1-core/2-variables.css b/assets/css/1-core/2-variables.css index d6e04f4b..3473979f 100644 --- a/assets/css/1-core/2-variables.css +++ b/assets/css/1-core/2-variables.css @@ -31,6 +31,15 @@ --green-30: hsl(140 100% 30%); --green-40: hsl(140 100% 40%); --red-60: hsl(358 80% 60%); + + /* Light backgrounds */ + --light-blue-background: hsl(210 100% 96%); + --light-red-background: hsl(355 73% 97%); + --light-green: hsl(206 41% 97%); + + /* Dark foregrounds to go with backgrounds */ + --dark-blue: hsl(220 64% 33%); + --dark-red: hsl(0 69% 36%); --background-color: var(--gray-100); --brand-border: var(--gray-100); --category-card-name-color: var(--gray-30); diff --git a/assets/css/2-components/7-button.css b/assets/css/2-components/7-button.css new file mode 100644 index 00000000..3cd25020 --- /dev/null +++ b/assets/css/2-components/7-button.css @@ -0,0 +1,32 @@ +/* stylelint-disable selector-class-pattern */ +/* stylelint-disable declaration-block-no-redundant-longhand-properties */ + +.button { + background-color: var(--main-page-button-background); + border-radius: 50rem; + border-width: 1px; + color: var(--text-color); + font-weight: bolder; + padding-bottom: 1rem; + padding-left: 2rem; + padding-right: 2rem; + padding-top: 1rem; +} + +.button:hover { + border-color: var(--main-page-button-focus-border-color); + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; + + /* offset-x | offset-y | blur-radius | spread-radius | color */ + box-shadow: 0 0 4px 2px var(--main-page-button-focus-border-color); +} + +.button:active { + border-color: var(--main-page-button-focus-border-color); + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; + + /* offset-x | offset-y | blur-radius | spread-radius | color */ + box-shadow: 0 0 4px 2px var(--main-page-button-focus-border-color); +} diff --git a/assets/css/2-components/8-alert.css b/assets/css/2-components/8-alert.css new file mode 100644 index 00000000..63dec2d5 --- /dev/null +++ b/assets/css/2-components/8-alert.css @@ -0,0 +1,27 @@ +.alert { + padding: 1rem; + border-radius: 0.5rem; + align-items: center; + display: flex; + margin-bottom: 1rem; +} + +.alert-info { + color: var(--dark-blue); + background-color: var(--light-blue-background); +} + +.alert-error { + color: var(--dark-red); + background-color: var(--light-red-background); +} + +svg.alert-icon { + flex-shrink: 0; + width: 1em; + height: 1em; +} + +.alert-message { + margin-inline-start: 0.75rem; +} diff --git a/assets/css/styles.css b/assets/css/styles.css index d69dcbbd..ef311f64 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -10,6 +10,8 @@ @import "2-components/4-license.css"; @import "2-components/5-primary-search.css"; @import "2-components/6-secondary-search.css"; +@import "2-components/7-button.css"; +@import "2-components/8-alert.css"; @import "3-screens/1-package/1-package.css"; @import "3-screens/1-package/2-release-changelog.css"; @@ -54,6 +56,14 @@ padding: 0.5rem; width: 100%; } + + .totp-zone { + display: none; + } + + input[type="checkbox"]:checked + div.totp-zone { + display: block; + } } .version-list-item { diff --git a/assets/yarn.lock b/assets/yarn.lock index 36942fb1..c4e879d4 100644 --- a/assets/yarn.lock +++ b/assets/yarn.lock @@ -2,41 +2,54 @@ # yarn lockfile v1 +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + "@babel/code-frame@^7.0.0": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" + integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== dependencies: - "@babel/highlight" "^7.18.6" + "@babel/highlight" "^7.23.4" + chalk "^2.4.2" -"@babel/helper-validator-identifier@^7.18.6": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== -"@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== +"@babel/highlight@^7.23.4": + version "7.23.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b" + integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A== dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" js-tokens "^4.0.0" -"@csstools/css-parser-algorithms@^2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.0.tgz#0cc3a656dc2d638370ecf6f98358973bfbd00141" - integrity sha512-dTKSIHHWc0zPvcS5cqGP+/TPFUJB0ekJ9dGKvMAFoNuBFhDPBt9OMGNZiIA5vTiNdGHHBeScYPXIGBMnVOahsA== +"@babel/runtime@^7.21.0": + version "7.23.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.5.tgz#11edb98f8aeec529b82b211028177679144242db" + integrity sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w== + dependencies: + regenerator-runtime "^0.14.0" -"@csstools/css-tokenizer@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz#07ae11a0a06365d7ec686549db7b729bc036528e" - integrity sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA== +"@csstools/css-parser-algorithms@^2.3.1": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.2.tgz#1e0d581dbf4518cb3e939c3b863cb7180c8cedad" + integrity sha512-sLYGdAdEY2x7TSw9FtmdaTrh2wFtRJO5VMbBrA8tEqEod7GEggFmxTSK9XqExib3yMuYNcvcTdCZIP6ukdjAIA== -"@csstools/media-query-list-parser@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.2.tgz#6ef642b728d30c1009bfbba3211c7e4c11302728" - integrity sha512-M8cFGGwl866o6++vIY7j1AKuq9v57cf+dGepScwCcbut9ypJNr4Cj+LLTWligYUZ0uyhEoJDKt5lvyBfh2L3ZQ== +"@csstools/css-tokenizer@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.2.1.tgz#9dc431c9a5f61087af626e41ac2a79cce7bb253d" + integrity sha512-Zmsf2f/CaEPWEVgw29odOj+WEVoiJy9s9NOv5GgNY9mZ1CZ7394By6wONrONrTsnNDv6F9hR02nvFihrGVGHBg== + +"@csstools/media-query-list-parser@^2.1.4": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.5.tgz#94bc8b3c3fd7112a40b7bf0b483e91eba0654a0f" + integrity sha512-IxVBdYzR8pYe89JiyXQuYk4aVVoCPhMJkz6ElRwlVysjwURTsTk/bmY/z4FfeRE+CRBMlykPwXEVUg8lThv7AQ== "@csstools/selector-specificity@^3.0.0": version "3.0.0" @@ -61,6 +74,50 @@ dependencies: purgecss "^4.1.3" +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -82,10 +139,15 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + "@ryangjchandler/alpine-clipboard@^2.1.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@ryangjchandler/alpine-clipboard/-/alpine-clipboard-2.2.0.tgz#92e54d02bb7ff9213b23d6069454e0be725a2ea9" - integrity sha512-2kKHd2mA6K7RuYlC+1fikIUPVJeJLQlY2w9rNGrOgVfzXUZRotjTP+EjxouDizTEvqNRkVTJnmmNle32Uhb4zw== + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ryangjchandler/alpine-clipboard/-/alpine-clipboard-2.3.0.tgz#44d7a9e8c4446fd24ece2d6be0d23fac7dd59b20" + integrity sha512-r1YL/LL851vSemjgcca+M6Yz9SNtA9ATul8nJ0n0sAS1W3V1GUWvH0Od2XdQF1r36YJF+/4sUc0eHF/Zexw7dA== "@tailwindcss/nesting@^0.0.0-insiders.565cd3e": version "0.0.0-insiders.565cd3e" @@ -100,24 +162,26 @@ integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== "@types/less@^3.0.3": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/less/-/less-3.0.3.tgz#f9451dbb9548d25391107d65d6401a0cfb15db92" - integrity sha512-1YXyYH83h6We1djyoUEqTlVyQtCfJAFXELSKW2ZRtjHD4hQ82CC4lvrv5D0l0FLcKBaiPbXyi3MpMsI9ZRgKsw== + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/less/-/less-3.0.6.tgz#279b51245ba787c810a0d286226c5900cd5e6765" + integrity sha512-PecSzorDGdabF57OBeQO/xFbAkYWo88g4Xvnsx7LRwqLC17I7OoKtA3bQB9uXkY6UkMWCOsA8HSVpaoitscdXw== "@types/minimist@^1.2.2": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" - integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.5.tgz#ec10755e871497bcd83efe927e43ec46e8c0747e" + integrity sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag== "@types/node@*": - version "18.15.11" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" - integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== + version "20.10.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.3.tgz#4900adcc7fc189d5af5bb41da8f543cea6962030" + integrity sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg== + dependencies: + undici-types "~5.26.4" "@types/normalize-package-data@^2.4.0": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" - integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + version "2.4.4" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" + integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== "@types/sass@^1.43.1": version "1.45.0" @@ -126,10 +190,10 @@ dependencies: sass "*" -"@types/stylus@^0.48.37": - version "0.48.38" - resolved "https://registry.yarnpkg.com/@types/stylus/-/stylus-0.48.38.tgz#6e62a59f9350f53a253aa42b038b6aa44a642c5b" - integrity sha512-B5otJekvD6XM8iTrnO6e2twoTY2tKL9VkL/57/2Lo4tv3EatbCaufdi68VVtn/h4yjO+HVvYEyrNQd0Lzj6riw== +"@types/stylus@^0.48.38": + version "0.48.42" + resolved "https://registry.yarnpkg.com/@types/stylus/-/stylus-0.48.42.tgz#8fa7d99b48556bb8fe85a052aaba8c1e59a97e2f" + integrity sha512-CPGlr5teL4sqdap+EOowMifLuNGeIoLwc0VQ7u/BPxo+ocqiNa5jeVt0H0IVBblEh6ZwX1sGpIQIFnSSr8NBQA== dependencies: "@types/node" "*" @@ -164,9 +228,9 @@ ajv@^8.0.1: uri-js "^4.2.2" alpinejs@^3.12.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/alpinejs/-/alpinejs-3.12.0.tgz#ff84d788231e7cc3fc38e363b7ebbc4f9d7031b2" - integrity sha512-YENcRBA9dlwR8PsZNFMTHbmdlTNwd1BkCeivPvOzzCKHas6AfwNRsDK9UEFmE5dXTMEZjnnpCTxV8vkdpWiOCw== + version "3.13.3" + resolved "https://registry.yarnpkg.com/alpinejs/-/alpinejs-3.13.3.tgz#92eb7e869b99ff548e7a55044e45660597cf530b" + integrity sha512-WZ6WQjkAOl+WdW/jukzNHq9zHFDNKmkk/x6WF7WdyNDD6woinrfXCVsZXm0galjbco+pEpYmJLtwlZwcOfIVdg== dependencies: "@vue/reactivity" "~3.1.1" @@ -175,6 +239,11 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -189,6 +258,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -268,13 +342,13 @@ atob@^2.1.2: integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== autoprefixer@^10.2.4, autoprefixer@^10.4.0: - version "10.4.14" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.14.tgz#e28d49902f8e759dd25b153264e862df2705f79d" - integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== + version "10.4.16" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.16.tgz#fad1411024d8670880bdece3970aa72e3572feb8" + integrity sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ== dependencies: - browserslist "^4.21.5" - caniuse-lite "^1.0.30001464" - fraction.js "^4.2.0" + browserslist "^4.21.10" + caniuse-lite "^1.0.30001538" + fraction.js "^4.3.6" normalize-range "^0.1.2" picocolors "^1.0.0" postcss-value-parser "^4.2.0" @@ -350,15 +424,15 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" -browserslist@^4.0.0, browserslist@^4.21.4, browserslist@^4.21.5: - version "4.21.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" - integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== +browserslist@^4.0.0, browserslist@^4.21.10, browserslist@^4.21.4: + version "4.22.2" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.2.tgz#704c4943072bd81ea18997f3bd2180e89c77874b" + integrity sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A== dependencies: - caniuse-lite "^1.0.30001449" - electron-to-chromium "^1.4.284" - node-releases "^2.0.8" - update-browserslist-db "^1.0.10" + caniuse-lite "^1.0.30001565" + electron-to-chromium "^1.4.601" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" cache-base@^1.0.1: version "1.0.1" @@ -410,12 +484,12 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464: - version "1.0.30001472" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001472.tgz#3f484885f2a2986c019dc416e65d9d62798cdd64" - integrity sha512-xWC/0+hHHQgj3/vrKYY0AAzeIUgr7L9wlELIcAvZdDUHlhL/kNxMdnQLOSOQfP8R51ZzPhmHdyMkI0MMpmxCfg== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001565: + version "1.0.30001566" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz#61a8e17caf3752e3e426d4239c549ebbb37fef0d" + integrity sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA== -chalk@^2.0.0, chalk@^2.4.1: +chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -512,7 +586,7 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== -color-name@^1.1.4, color-name@~1.1.4: +color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -538,9 +612,9 @@ commander@^8.0.0: integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== component-emitter@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" - integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + version "1.3.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.1.tgz#ef1d5796f7d93f135ee6fb684340b26403c97d17" + integrity sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== concat-map@0.0.1: version "0.0.1" @@ -567,19 +641,19 @@ copy-descriptor@^0.1.0: integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== cosmiconfig@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd" - integrity sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ== + version "8.3.6" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.3.6.tgz#060a2b871d66dba6c8538ea1118ba1ac16f5fae3" + integrity sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA== dependencies: - import-fresh "^3.2.1" + import-fresh "^3.3.0" js-yaml "^4.1.0" - parse-json "^5.0.0" + parse-json "^5.2.0" path-type "^4.0.0" cpx2@^4.2.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/cpx2/-/cpx2-4.2.2.tgz#bcb442a4c28312a6acf2cab053875aff9a87e5c7" - integrity sha512-pFYHCivwNALi+yM4kATIA4XQvA72lhjcBBlwHRHtrvTcHWlMQfpgyyUHhus+iC7pHVBYnoGpza7ZrWBsJJAg5Q== + version "4.2.3" + resolved "https://registry.yarnpkg.com/cpx2/-/cpx2-4.2.3.tgz#cf7fc1321e396858ffefdbcfe935a2475a0a0435" + integrity sha512-UM7Iza+OM8FZ2ntTml/mdb3RmSLK5I2DqFqDdMihlGyKZCAAnDP++H973Oyc/2TQpEMtg5JHeRNfewclE330EA== dependencies: debounce "^1.2.0" debug "^4.1.1" @@ -588,22 +662,31 @@ cpx2@^4.2.0: glob-gitignore "^1.0.14" glob2base "0.0.12" ignore "^5.1.8" - minimatch "^7.4.2" + minimatch "^8.0.2" p-map "^4.0.0" resolve "^1.12.0" safe-buffer "^5.2.0" shell-quote "^1.8.0" subarg "^1.0.0" +cross-spawn@^7.0.0: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + css-declaration-sorter@^6.3.1: - version "6.4.0" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.0.tgz#630618adc21724484b3e9505bce812def44000ad" - integrity sha512-jDfsatwWMWN0MODAFuHszfjphEXfNw9JUAhmY4pLu3TyTU+ohUpsbVtbU+1MZn4a47D9kqh03i4eyOm+74+zew== + version "6.4.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71" + integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g== -css-functions-list@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.1.0.tgz#cf5b09f835ad91a00e5959bcfc627cd498e1321b" - integrity sha512-/9lCvYZaUbBGvYUgYGFJ4dcYiyqdhSjG7IPVluoV8A1ILjkF7ilmhp1OGUz8n+nmBcu0RNrQAzgD8B6FJbrt2w== +css-functions-list@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.1.tgz#2eb205d8ce9f9ce74c5c1d7490b66b77c45ce3ea" + integrity sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ== css-select@^4.1.3: version "4.3.0" @@ -707,9 +790,11 @@ csso@^4.2.0: css-tree "^1.1.2" date-fns@^2.16.1: - version "2.29.3" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" - integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" debounce@^1.2.0: version "1.2.1" @@ -832,16 +917,26 @@ duplexer@^0.1.1: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== -electron-to-chromium@^1.4.284: - version "1.4.342" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.342.tgz#3c7e199c3aa89c993df4b6f5223d6d26988f58e6" - integrity sha512-dTei3VResi5bINDENswBxhL+N0Mw5YnfWyTqO75KGsVldurEkhC9+CelJVAse8jycWyP8pv3VSj4BSyP8wTWJA== +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +electron-to-chromium@^1.4.601: + version "1.4.603" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.603.tgz#446907c21d333b55d0beaba1cb5b48430775a8a7" + integrity sha512-Dvo5OGjnl7AZTU632dFJtWj0uJK835eeOVQIuRcmBmsFsTNn3cL05FqOyHAfGQDIoHfLhyJ1Tya3PJ0ceMz54g== emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -925,21 +1020,21 @@ esbuild-openbsd-64@0.13.15: integrity sha512-wTfvtwYJYAFL1fSs8yHIdf5GEE4NkbtbXtjLWjM3Cw8mmQKqsg8kTiqJ9NJQe5NX/5Qlo7Xd9r1yKMMkHllp5g== esbuild-plugin-assets-manifest@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/esbuild-plugin-assets-manifest/-/esbuild-plugin-assets-manifest-1.0.7.tgz#a896616bbfff86427251177936ff0447b1bdfa74" - integrity sha512-UIuXaCeyWW8ydfJjU5h4/NMLHGvi8nFgDhTVbvcfab03DAkXrMP1/5hnLZ5FpAC63nnOnwmOZk4pLx4amWAyaA== + version "1.0.8" + resolved "https://registry.yarnpkg.com/esbuild-plugin-assets-manifest/-/esbuild-plugin-assets-manifest-1.0.8.tgz#af597aa2753f2e087c04a31f712a54ab5ffbdab1" + integrity sha512-2goWUmBLpqaHASkMQIgsSqcAs3Y1rDMKcOjk8cWOTtAzmbHYQLh77MEjuQYDx3bBEQ9dVZCaU0/cBIjhvdJf+w== esbuild-style-plugin@^1.6.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/esbuild-style-plugin/-/esbuild-style-plugin-1.6.1.tgz#9a0f6f47587890c56d10a4ae3fc4ce5697baaa28" - integrity sha512-t5ZtQyGKNiM8DLedz+iwFH0LPWLnMmaEQ2RnnP1ppFHq35najxqJHAhUVVSbTFm5cR2ZumGdBMqnmuKBRBMvDw== + version "1.6.3" + resolved "https://registry.yarnpkg.com/esbuild-style-plugin/-/esbuild-style-plugin-1.6.3.tgz#123c994047f1393bee6896c1c37bd4f1942c425f" + integrity sha512-XPEKf4FjLjEVLv/dJH4UxDzXCrFHYpD93DBO8B+izdZARW5b7nNKQbnKv3J+7VDWJbgCU+hzfgIh2AuIZzlmXQ== dependencies: "@types/less" "^3.0.3" "@types/sass" "^1.43.1" - "@types/stylus" "^0.48.37" - glob "^8.0.1" - postcss "^8.4.12" - postcss-modules "^4.3.1" + "@types/stylus" "^0.48.38" + glob "^10.2.2" + postcss "^8.4.31" + postcss-modules "^6.0.0" esbuild-sunos-64@0.13.15: version "0.13.15" @@ -1041,10 +1136,10 @@ fast-deep-equal@^3.1.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.12, fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.0.tgz#7c40cb491e1e2ed5664749e87bfb516dbe8727c0" - integrity sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA== +fast-glob@^3.2.7, fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -1064,12 +1159,12 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== +file-entry-cache@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-7.0.2.tgz#2d61bb70ba89b9548e3035b7c9173fe91deafff0" + integrity sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g== dependencies: - flat-cache "^3.0.4" + flat-cache "^3.2.0" fill-range@^4.0.0: version "4.0.0" @@ -1101,28 +1196,37 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== +flat-cache@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: - flatted "^3.1.0" + flatted "^3.2.9" + keyv "^4.5.3" rimraf "^3.0.2" -flatted@^3.1.0: - version "3.2.7" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== +flatted@^3.2.9: + version "3.2.9" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" + integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" integrity sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ== -fraction.js@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" - integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +fraction.js@^4.3.6: + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== fragment-cache@^0.2.1: version "0.2.1" @@ -1141,9 +1245,9 @@ fs-extra@^10.0.0: universalify "^2.0.0" fs-extra@^11.1.0: - version "11.1.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" - integrity sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ== + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== dependencies: graceful-fs "^4.2.0" jsonfile "^6.0.1" @@ -1165,14 +1269,14 @@ fs.realpath@^1.0.0: integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== generic-names@^4.0.0: version "4.0.0" @@ -1241,6 +1345,17 @@ glob@7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^10.2.2: + version "10.3.10" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" + integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.5" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" + glob@^7.1.3, glob@^7.1.7: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -1253,17 +1368,6 @@ glob@^7.1.3, glob@^7.1.7: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.1: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - global-modules@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -1360,12 +1464,12 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== dependencies: - function-bind "^1.1.1" + function-bind "^1.1.2" hosted-git-info@^4.0.1: version "4.1.0" @@ -1379,27 +1483,22 @@ html-tags@^3.3.1: resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== -icss-replace-symbols@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" - integrity sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg== - -icss-utils@^5.0.0: +icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== ignore@^5.0.5, ignore@^5.1.8, ignore@^5.1.9, ignore@^5.2.0, ignore@^5.2.4: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + version "5.3.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78" + integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg== immutable@^4.0.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" - integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== + version "4.3.4" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" + integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA== -import-fresh@^3.2.1: +import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -1445,19 +1544,12 @@ ini@^1.3.5: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -is-accessor-descriptor@^0.1.6: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" - integrity sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A== - dependencies: - kind-of "^3.0.2" - -is-accessor-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" - integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== +is-accessor-descriptor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz#3223b10628354644b86260db29b3e693f5ceedd4" + integrity sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA== dependencies: - kind-of "^6.0.0" + hasown "^2.0.0" is-arrayish@^0.2.1: version "0.2.1" @@ -1476,44 +1568,35 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== -is-core-module@^2.5.0, is-core-module@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" - integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== +is-core-module@^2.13.0, is-core-module@^2.5.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: - has "^1.0.3" - -is-data-descriptor@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" - integrity sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg== - dependencies: - kind-of "^3.0.2" + hasown "^2.0.0" -is-data-descriptor@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" - integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== +is-data-descriptor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz#2109164426166d32ea38c405c1e0945d9e6a4eeb" + integrity sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw== dependencies: - kind-of "^6.0.0" + hasown "^2.0.0" is-descriptor@^0.1.0: - version "0.1.6" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" - integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + version "0.1.7" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.7.tgz#2727eb61fd789dcd5bdf0ed4569f551d2fe3be33" + integrity sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg== dependencies: - is-accessor-descriptor "^0.1.6" - is-data-descriptor "^0.1.4" - kind-of "^5.0.0" + is-accessor-descriptor "^1.0.1" + is-data-descriptor "^1.0.1" is-descriptor@^1.0.0, is-descriptor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" - integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.3.tgz#92d27cb3cd311c4977a4db47df457234a13cb306" + integrity sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw== dependencies: - is-accessor-descriptor "^1.0.0" - is-data-descriptor "^1.0.0" - kind-of "^6.0.2" + is-accessor-descriptor "^1.0.1" + is-data-descriptor "^1.0.1" is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" @@ -1600,10 +1683,19 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== -jiti@^1.17.2: - version "1.18.2" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.18.2.tgz#80c3ef3d486ebf2450d9335122b32d121f2a83cd" - integrity sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg== +jackspeak@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" + integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jiti@^1.19.1: + version "1.21.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" + integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== js-tokens@^4.0.0: version "4.0.0" @@ -1617,6 +1709,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -1636,6 +1733,13 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -1650,26 +1754,26 @@ kind-of@^4.0.0: dependencies: is-buffer "^1.1.5" -kind-of@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" - integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== - -kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: +kind-of@^6.0.2, kind-of@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -known-css-properties@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.27.0.tgz#82a9358dda5fe7f7bd12b5e7142c0a205393c0c5" - integrity sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg== +known-css-properties@^0.29.0: + version "0.29.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.29.0.tgz#e8ba024fb03886f23cb882e806929f32d814158f" + integrity sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ== -lilconfig@^2.0.3, lilconfig@^2.0.5, lilconfig@^2.0.6: +lilconfig@^2.0.3, lilconfig@^2.0.5, lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== +lilconfig@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.0.0.tgz#f8067feb033b5b74dab4602a5f5029420be749bc" + integrity sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -1729,6 +1833,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +"lru-cache@^9.1.1 || ^10.0.0": + version "10.1.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484" + integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== + make-array@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/make-array/-/make-array-1.0.5.tgz#326a7635c756a9f61ce0b2a6fdd5cc3460419bcb" @@ -1833,17 +1942,17 @@ minimatch@^3.0.4, minimatch@^3.1.1: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== +minimatch@^8.0.2: + version "8.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-8.0.4.tgz#847c1b25c014d4e9a7f68aaf63dedd668a626229" + integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== dependencies: brace-expansion "^2.0.1" -minimatch@^7.4.2: - version "7.4.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.3.tgz#012cbf110a65134bb354ae9773b55256cdb045a2" - integrity sha512-5UB4yYusDtkRPbRiy1cqZ1IpGNcJCGlEMG17RKzPddpyiPKoCdwohbED8g4QXT0ewCt8LTkQXuljsUfQ3FKM4A== +minimatch@^9.0.1: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== dependencies: brace-expansion "^2.0.1" @@ -1861,6 +1970,11 @@ minimist@^1.1.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.0.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== + mixin-deep@^1.2.0: version "1.3.2" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" @@ -1895,10 +2009,10 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== nanomatch@^1.2.9: version "1.2.13" @@ -1917,10 +2031,10 @@ nanomatch@^1.2.9: snapdragon "^0.8.1" to-regex "^3.0.1" -node-releases@^2.0.8: - version "2.0.10" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" - integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== normalize-package-data@^3.0.2: version "3.0.3" @@ -2022,7 +2136,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-json@^5.0.0, parse-json@^5.2.0: +parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -2052,11 +2166,24 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" + integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== + dependencies: + lru-cache "^9.1.1 || ^10.0.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -2083,9 +2210,9 @@ pify@^3.0.0: integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg== pirates@^4.0.1: - version "4.0.5" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" - integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== posix-character-classes@^0.1.0: version "0.1.1" @@ -2174,7 +2301,7 @@ postcss-hash@^3.0.0: dependencies: mkdirp "^0.5.1" -postcss-import@^14.0.2, postcss-import@^14.1.0: +postcss-import@^14.0.2: version "14.1.0" resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0" integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw== @@ -2183,14 +2310,23 @@ postcss-import@^14.0.2, postcss-import@^14.1.0: read-cache "^1.0.0" resolve "^1.1.7" -postcss-js@^4.0.0: +postcss-import@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" + integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== dependencies: camelcase-css "^2.0.1" -postcss-load-config@^3.0.0, postcss-load-config@^3.1.4: +postcss-load-config@^3.0.0: version "3.1.4" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== @@ -2198,6 +2334,14 @@ postcss-load-config@^3.0.0, postcss-load-config@^3.1.4: lilconfig "^2.0.5" yaml "^1.10.2" +postcss-load-config@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" + integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== + dependencies: + lilconfig "^3.0.0" + yaml "^2.3.4" + postcss-merge-longhand@^5.1.7: version "5.1.7" resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz#24a1bdf402d9ef0e70f568f39bdc0344d568fb16" @@ -2254,9 +2398,9 @@ postcss-modules-extract-imports@^3.0.0: integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== postcss-modules-local-by-default@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz#ebbb54fae1598eecfdf691a02b3ff3b390a5a51c" - integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524" + integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA== dependencies: icss-utils "^5.0.0" postcss-selector-parser "^6.0.2" @@ -2276,13 +2420,13 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" -postcss-modules@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-4.3.1.tgz#517c06c09eab07d133ae0effca2c510abba18048" - integrity sha512-ItUhSUxBBdNamkT3KzIZwYNNRFKmkJrofvC2nWab3CPKhYBQ1f27XXh1PAPE27Psx58jeelPsxWB/+og+KEH0Q== +postcss-modules@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-6.0.0.tgz#cac283dbabbbdc2558c45391cbd0e2df9ec50118" + integrity sha512-7DGfnlyi/ju82BRzTIjWS5C4Tafmzl3R79YP/PASiocj+aa6yYphHhhKUOEoXQToId5rgyFgJ88+ccOUydjBXQ== dependencies: generic-names "^4.0.0" - icss-replace-symbols "^1.1.0" + icss-utils "^5.1.0" lodash.camelcase "^4.3.0" postcss-modules-extract-imports "^3.0.0" postcss-modules-local-by-default "^4.0.0" @@ -2290,13 +2434,6 @@ postcss-modules@^4.3.1: postcss-modules-values "^4.0.0" string-hash "^1.1.1" -postcss-nested@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.0.tgz#1572f1984736578f360cffc7eb7dca69e30d1735" - integrity sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w== - dependencies: - postcss-selector-parser "^6.0.10" - postcss-nested@^5.0.5: version "5.0.6" resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-5.0.6.tgz#466343f7fc8d3d46af3e7dba3fcd47d052a945bc" @@ -2304,6 +2441,13 @@ postcss-nested@^5.0.5: dependencies: postcss-selector-parser "^6.0.6" +postcss-nested@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" + integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== + dependencies: + postcss-selector-parser "^6.0.11" + postcss-normalize-charset@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" @@ -2408,7 +2552,7 @@ postcss-safe-parser@^6.0.0: resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1" integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== -postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.13, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: +postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.13, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: version "6.0.13" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== @@ -2450,19 +2594,19 @@ postcss@^6.0.3: source-map "^0.6.1" supports-color "^5.4.0" -postcss@^8.0.9, postcss@^8.2.4, postcss@^8.3.5, postcss@^8.4.12, postcss@^8.4.24, postcss@^8.4.31: - version "8.4.31" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" - integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== +postcss@^8.2.4, postcss@^8.3.5, postcss@^8.4.23, postcss@^8.4.28, postcss@^8.4.31: + version "8.4.32" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9" + integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw== dependencies: - nanoid "^3.3.6" + nanoid "^3.3.7" picocolors "^1.0.0" source-map-js "^1.0.2" prettier@^2.7.1: - version "2.8.7" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450" - integrity sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw== + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== pretty-hrtime@^1.0.3: version "1.0.3" @@ -2470,9 +2614,9 @@ pretty-hrtime@^1.0.3: integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A== punycode@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== purgecss@^4.1.3: version "4.1.3" @@ -2535,6 +2679,11 @@ redent@^4.0.0: indent-string "^5.0.0" strip-indent "^4.0.0" +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" @@ -2578,12 +2727,12 @@ resolve-url@^0.2.1: resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg== -resolve@^1.1.7, resolve@^1.12.0, resolve@^1.22.1: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== +resolve@^1.1.7, resolve@^1.12.0, resolve@^1.22.2: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -2631,9 +2780,9 @@ safe-regex@^1.1.0: ret "~0.1.10" sass@*: - version "1.60.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.60.0.tgz#657f0c23a302ac494b09a5ba8497b739fb5b5a81" - integrity sha512-updbwW6fNb5gGm8qMXzVO7V4sWf7LMXnMly/JEyfbfERbVH46Fn6q02BX7/eHTdKpE7d+oTkMMQpFWNUMfFbgQ== + version "1.69.5" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.5.tgz#23e18d1c757a35f2e52cc81871060b9ad653dfde" + integrity sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -2656,15 +2805,27 @@ set-value@^2.0.0, set-value@^2.0.1: is-plain-object "^2.0.3" split-string "^3.0.1" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + shell-quote@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.0.tgz#20d078d0eaf71d54f43bd2ba14a1b5b9bfa5c8ba" - integrity sha512-QHsz8GgQIGKlRi24yFc6a6lN69Idnx634w49ay6+jA5yFh7a1UY+4Rp6HPx/L/1zcEDPEij8cIsiqR6bQsE5VQ== + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== signal-exit@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" - integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== slash@^3.0.0: version "3.0.0" @@ -2773,9 +2934,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.13" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz#7189a474c46f8d47c7b0da4b987bb45e908bd2d5" - integrity sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w== + version "3.0.16" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f" + integrity sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw== split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" @@ -2802,7 +2963,7 @@ string-hash@^1.1.1: resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A== -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -2811,13 +2972,29 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + strip-indent@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-4.0.0.tgz#b41379433dd06f5eae805e21d631e07ee670d853" @@ -2856,23 +3033,23 @@ stylelint-config-standard@^26.0.0: stylelint-config-recommended "^8.0.0" stylelint@^15.10.1: - version "15.10.1" - resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.10.1.tgz#93f189958687e330c106b010cbec0c41dcae506d" - integrity sha512-CYkzYrCFfA/gnOR+u9kJ1PpzwG10WLVnoxHDuBA/JiwGqdM9+yx9+ou6SE/y9YHtfv1mcLo06fdadHTOx4gBZQ== + version "15.11.0" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.11.0.tgz#3ff8466f5f5c47362bc7c8c9d382741c58bc3292" + integrity sha512-78O4c6IswZ9TzpcIiQJIN49K3qNoXTM8zEJzhaTE/xRTCZswaovSEVIa/uwbOltZrk16X4jAxjaOhzz/hTm1Kw== dependencies: - "@csstools/css-parser-algorithms" "^2.3.0" - "@csstools/css-tokenizer" "^2.1.1" - "@csstools/media-query-list-parser" "^2.1.2" + "@csstools/css-parser-algorithms" "^2.3.1" + "@csstools/css-tokenizer" "^2.2.0" + "@csstools/media-query-list-parser" "^2.1.4" "@csstools/selector-specificity" "^3.0.0" balanced-match "^2.0.0" colord "^2.9.3" cosmiconfig "^8.2.0" - css-functions-list "^3.1.0" + css-functions-list "^3.2.1" css-tree "^2.3.1" debug "^4.3.4" - fast-glob "^3.3.0" + fast-glob "^3.3.1" fastest-levenshtein "^1.0.16" - file-entry-cache "^6.0.1" + file-entry-cache "^7.0.0" global-modules "^2.0.0" globby "^11.1.0" globjoin "^0.1.4" @@ -2881,13 +3058,13 @@ stylelint@^15.10.1: import-lazy "^4.0.0" imurmurhash "^0.1.4" is-plain-object "^5.0.0" - known-css-properties "^0.27.0" + known-css-properties "^0.29.0" mathml-tag-names "^2.1.3" meow "^10.1.5" micromatch "^4.0.5" normalize-path "^3.0.0" picocolors "^1.0.0" - postcss "^8.4.24" + postcss "^8.4.28" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^6.0.0" postcss-selector-parser "^6.0.13" @@ -2908,11 +3085,12 @@ subarg@^1.0.0: dependencies: minimist "^1.1.0" -sucrase@^3.29.0: - version "3.31.0" - resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.31.0.tgz#daae4fd458167c5d4ba1cce6aef57b988b417b33" - integrity sha512-6QsHnkqyVEzYcaiHsOKkzOtOgdJcb8i54x6AV2hDwyZcY9ZyykGZVw6L/YN98xC0evwTP6utsWWrKRaa8QlfEQ== +sucrase@^3.32.0: + version "3.34.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f" + integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw== dependencies: + "@jridgewell/gen-mapping" "^0.3.2" commander "^4.0.0" glob "7.1.6" lines-and-columns "^1.1.6" @@ -2984,34 +3162,32 @@ table@^6.8.1: strip-ansi "^6.0.1" tailwindcss@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.0.tgz#8cab40e5a10a10648118c0859ba8bfbc744a761e" - integrity sha512-hOXlFx+YcklJ8kXiCAfk/FMyr4Pm9ck477G0m/us2344Vuj355IpoEDB5UmGAsSpTBmr+4ZhjzW04JuFXkb/fw== + version "3.3.6" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.3.6.tgz#4dd7986bf4902ad385d90d45fd4b2fa5fab26d5f" + integrity sha512-AKjF7qbbLvLaPieoKeTjG1+FyNZT6KaJMJPFeQyLfIp7l82ggH1fbHJSsYIvnbTFQOlkh+gBYpyby5GT1LIdLw== dependencies: + "@alloc/quick-lru" "^5.2.0" arg "^5.0.2" chokidar "^3.5.3" - color-name "^1.1.4" didyoumean "^1.2.2" dlv "^1.1.3" - fast-glob "^3.2.12" + fast-glob "^3.3.0" glob-parent "^6.0.2" is-glob "^4.0.3" - jiti "^1.17.2" - lilconfig "^2.0.6" + jiti "^1.19.1" + lilconfig "^2.1.0" micromatch "^4.0.5" normalize-path "^3.0.0" object-hash "^3.0.0" picocolors "^1.0.0" - postcss "^8.0.9" - postcss-import "^14.1.0" - postcss-js "^4.0.0" - postcss-load-config "^3.1.4" - postcss-nested "6.0.0" + postcss "^8.4.23" + postcss-import "^15.1.0" + postcss-js "^4.0.1" + postcss-load-config "^4.0.1" + postcss-nested "^6.0.1" postcss-selector-parser "^6.0.11" - postcss-value-parser "^4.2.0" - quick-lru "^5.1.1" - resolve "^1.22.1" - sucrase "^3.29.0" + resolve "^1.22.2" + sucrase "^3.32.0" thenby@^1.3.4: version "1.3.4" @@ -3096,6 +3272,11 @@ type-fest@^1.0.1, type-fest@^1.2.1, type-fest@^1.2.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + union-value@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" @@ -3107,9 +3288,9 @@ union-value@^1.0.0: set-value "^2.0.1" universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== unset-value@^1.0.0: version "1.0.0" @@ -3119,10 +3300,10 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -update-browserslist-db@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -3169,7 +3350,14 @@ which@^1.3.1: dependencies: isexe "^2.0.0" -wrap-ansi@^7.0.0: +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -3178,6 +3366,15 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -3206,6 +3403,11 @@ yaml@^1.10.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yaml@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" + integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA== + yargs-parser@^20.2.2, yargs-parser@^20.2.9: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" @@ -3230,9 +3432,9 @@ yargs@^16.2.0: yargs-parser "^20.2.2" yargs@^17.0.0: - version "17.7.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.1.tgz#34a77645201d1a8fc5213ace787c220eabbd0967" - integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw== + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== dependencies: cliui "^8.0.1" escalade "^3.1.1" diff --git a/cabal.project b/cabal.project index 0f0dcfeb..9221f5fa 100644 --- a/cabal.project +++ b/cabal.project @@ -1,6 +1,8 @@ -packages: ./ +packages: + ./ + https://hackage.haskell.org/package/sel-0.0.1.0/candidate/sel-0.0.1.0.tar.gz -with-compiler: ghc-9.4.5 +with-compiler: ghc-9.4.7 tests: True @@ -62,3 +64,8 @@ source-repository-package type: git location: /~https://github.com/saurabhnanda/odd-jobs tag: 51c7443 + +source-repository-package + type: git + location: /~https://github.com/haskell-cryptography/one-time-password + tag: 2ca2313 diff --git a/cabal.project.freeze b/cabal.project.freeze index 5d943a81..6d2928a8 100644 --- a/cabal.project.freeze +++ b/cabal.project.freeze @@ -4,20 +4,23 @@ constraints: any.Cabal ==3.8.1.0, any.HUnit ==1.6.2.0, any.OneTuple ==0.4.1.1, any.Only ==0.1, - any.PyF ==0.11.1.1, + any.PyF ==0.11.2.1, PyF -python_test, any.QuickCheck ==2.14.3, QuickCheck -old-random +templatehaskell, + any.RSA ==2.4.1, + any.SHA ==1.6.4.4, + SHA -exe, any.StateVar ==1.2.2, any.abstract-deque ==0.3, abstract-deque -usecas, any.adjunctions ==4.4.2, any.aeson ==2.1.2.1, aeson -cffi +ordered-keymap, - any.aeson-pretty ==0.8.9, + any.aeson-pretty ==0.8.10, aeson-pretty -lib-only, - any.ansi-terminal ==0.11.5, - ansi-terminal -example +win32-2-13-1, + any.ansi-terminal ==1.0, + ansi-terminal -example, any.ansi-terminal-types ==0.11.5, any.appar ==0.1.8, any.array ==0.5.4.0, @@ -32,13 +35,16 @@ constraints: any.Cabal ==3.8.1.0, atomic-primops -debug, any.attoparsec ==0.14.4, attoparsec -developer, - any.attoparsec-iso8601 ==1.1.0.0, + any.attoparsec-aeson ==2.1.0.0, + any.attoparsec-iso8601 ==1.1.0.1, + any.authenticate-oauth ==1.7, any.auto-update ==0.1.6, - any.barbies ==2.0.4.0, - any.base ==4.17.1.0, - any.base-compat ==0.12.2, - any.base-compat-batteries ==0.12.2, - any.base-orphans ==0.9.0, + any.barbies ==2.0.5.0, + any.base ==4.17.2.0, + any.base-compat ==0.12.3, + any.base-compat-batteries ==0.12.3, + any.base-orphans ==0.9.1, + any.base16 ==1.0, any.base16-bytestring ==1.0.2.0, any.base64 ==0.4.2.4, any.base64-bytestring ==1.2.1.0, @@ -46,16 +52,21 @@ constraints: any.Cabal ==3.8.1.0, any.bifunctors ==5.5.15, bifunctors +semigroups +tagged, any.binary ==0.8.9.1, - any.bitvec ==1.1.4.0, - bitvec -libgmp, - any.blaze-builder ==0.4.2.2, + any.bitvec ==1.1.5.0, + bitvec +simd, + any.blaze-builder ==0.4.2.3, any.blaze-html ==0.9.1.2, - any.blaze-markup ==0.8.2.8, + any.blaze-markup ==0.8.3.0, any.boring ==0.2.1, boring +tagged, any.bsb-http-chunked ==0.0.0.4, + any.bytebuild ==0.3.14.0, + bytebuild -checked, any.byteorder ==1.0.4, - any.bytestring ==0.11.4.0, + any.byteslice ==0.2.11.1, + byteslice +avoid-rawmemchr, + any.bytesmith ==0.3.10.0, + any.bytestring ==0.11.5.2, any.bytestring-builder ==0.10.8.2.0, bytestring-builder +bytestring_has_builder, any.cabal-doctest ==1.0.9, @@ -63,29 +74,36 @@ constraints: any.Cabal ==3.8.1.0, any.case-insensitive ==1.2.1.0, any.cereal ==0.5.8.3, cereal -bytestring-builder, - any.clock ==0.8.3, + any.chronos ==1.1.5.1, + any.clock ==0.8.4, clock -llvm, - any.cmark-gfm ==0.2.5, + any.cmark-gfm ==0.2.6, cmark-gfm -pkgconfig, any.cmdargs ==0.10.22, cmdargs +quotation -testprog, any.colour ==2.3.6, any.colourista ==0.1.0.2, - any.commonmark ==0.2.2, - any.commonmark-extensions ==0.2.3.4, + any.commonmark ==0.2.4, + any.commonmark-extensions ==0.2.4, any.comonad ==5.0.8, comonad +containers +distributive +indexed-traversable, - any.concurrent-output ==1.10.18, + any.concurrent-output ==1.10.20, any.conduit ==1.3.5, any.conduit-extra ==1.3.6, any.constraints ==0.13.4, any.containers ==0.6.7, + any.contiguous ==0.6.4.0, any.contravariant ==1.5.5, contravariant +semigroups +statevar +tagged, any.cookie ==0.4.6, + any.crypto-api ==0.13.3, + crypto-api -all_cpolys, + any.crypto-pubkey-types ==0.4.3, any.cryptohash-md5 ==0.11.101.0, any.cryptohash-sha1 ==0.11.101.0, - any.crypton ==0.31, + any.cryptohash-sha256 ==0.11.102.1, + cryptohash-sha256 -exe +use-cbits, + any.crypton ==0.34, crypton -check_alignment +integer-gmp -old_toolchain_inliner +support_aesni +support_deepseq +support_pclmuldq +support_rdrand -support_sse +use_target_attributes, any.crypton-connection ==0.3.1, any.crypton-x509 ==1.7.6, @@ -95,7 +113,6 @@ constraints: any.Cabal ==3.8.1.0, any.cryptonite ==0.30, cryptonite -check_alignment +integer-gmp -old_toolchain_inliner +support_aesni +support_deepseq -support_pclmuldq +support_rdrand -support_sse +use_target_attributes, any.cryptonite-conduit ==0.2.2, - any.daemons ==0.3.0, any.data-default ==0.7.1.1, any.data-default-class ==0.1.2.0, any.data-default-instances-containers ==0.0.1, @@ -106,6 +123,7 @@ constraints: any.Cabal ==3.8.1.0, any.data-sketches-core ==0.1.0.0, any.dec ==0.0.5, any.deepseq ==1.4.8.0, + any.deriving-aeson ==0.2.9, any.directory ==1.3.7.1, any.distributive ==0.6.2.1, distributive +semigroups +tagged, @@ -116,13 +134,15 @@ constraints: any.Cabal ==3.8.1.0, effectful -benchmark-foreign-libraries, any.effectful-core ==2.2.2.2, any.either ==5.0.2, - any.emojis ==0.1.2, + any.emojis ==0.1.3, any.entropy ==0.4.1.10, entropy -donotgetentropy, any.envparse ==0.5.0, any.erf ==2.0.0.0, any.exceptions ==0.10.5, - any.fast-logger ==3.2.1, + any.extensible-exceptions ==0.1.1.4, + any.extra ==1.7.14, + any.fast-logger ==3.2.2, any.file-embed ==0.0.15.0, any.filepath ==1.4.2.2, any.filtrable ==0.1.6.0, @@ -134,52 +154,68 @@ constraints: any.Cabal ==3.8.1.0, any.free ==5.1.10, any.friendly-time ==0.4.1, any.fusion-plugin-types ==0.1.0, - any.generic-deriving ==1.14.4, + any.generic-deriving ==1.14.5, generic-deriving +base-4-9, any.generically ==0.1.1, - any.ghc ==9.4.5, + any.generics-sop ==0.5.1.3, + any.ghc ==9.4.7, any.ghc-bignum ==1.3, - any.ghc-boot ==9.4.5, - any.ghc-boot-th ==9.4.5, - any.ghc-heap ==9.4.5, - any.ghc-prim ==0.9.0, - any.ghci ==9.4.5, + any.ghc-boot ==9.4.7, + any.ghc-boot-th ==9.4.7, + any.ghc-heap ==9.4.7, + any.ghc-prim ==0.9.1, + any.ghci ==9.4.7, any.haddock-library ==1.11.0, any.happy ==1.20.1.1, - any.hashable ==1.4.2.0, + any.hashable ==1.4.3.0, hashable +integer-gmp -random-initial-seed, any.haskell-lexer ==1.1.1, + any.haskell-src-exts ==1.23.1, + any.haskell-src-meta ==0.8.13, + any.hdaemonize ==0.5.7, any.heaps ==0.4, - any.hedgehog ==1.2, + any.hedgehog ==1.4, any.hostname ==1.0, any.hourglass ==0.2.12, any.hpc ==0.6.1.0, - any.hsc2hs ==0.68.9, + any.hsc2hs ==0.68.10, hsc2hs -in-ghc-tree, + any.hspec ==2.11.7, + any.hspec-core ==2.11.7, + any.hspec-discover ==2.11.7, + any.hspec-expectations ==0.8.4, + any.hsyslog ==5.0.2, + hsyslog -install-examples, any.http-api-data ==0.5, http-api-data -use-text-show, - any.http-client ==0.7.13.1, + any.http-client ==0.7.15, http-client +network-uri, - any.http-client-tls ==0.3.6.2, - any.http-conduit ==2.3.8.2, + any.http-client-tls ==0.3.6.3, + any.http-conduit ==2.3.8.3, http-conduit +aeson, any.http-date ==0.0.11, - any.http-media ==0.8.0.0, + any.http-media ==0.8.1.1, any.http-types ==0.12.3, - any.http2 ==4.1.4, + any.http2 ==4.2.2, http2 -devel -h2spec, - any.indexed-profunctors ==0.1.1, - any.indexed-traversable ==0.1.2.1, + any.indexed-profunctors ==0.1.1.1, + any.indexed-traversable ==0.1.3, any.indexed-traversable-instances ==0.1.1.2, + any.insert-ordered-containers ==0.2.5.3, + any.integer-conversion ==0.1.0.1, any.integer-gmp ==1.1, any.integer-logarithms ==1.0.3.1, integer-logarithms -check-bounds +integer-gmp, - any.invariant ==0.6.1, + any.invariant ==0.6.2, any.iproute ==1.7.12, any.iso8601-time ==0.1.5, iso8601-time +new-time, any.kan-extensions ==5.2.5, - any.lifted-async ==0.10.2.4, + any.lens ==5.2.3, + lens -benchmark-uniplate -dump-splices +inlining -j +test-hunit +test-properties +test-templates +trustworthy, + any.libsodium-bindings ==0.0.1.0, + libsodium-bindings -homebrew-libsodium -use-pkg-config, + any.lifted-async ==0.10.2.5, any.lifted-base ==0.2.3.12, any.lockfree-queue ==0.2.4, any.log-base ==0.12.0.1, @@ -187,13 +223,17 @@ constraints: any.Cabal ==3.8.1.0, any.lucid ==2.11.20230408, any.lucid-alpine ==0.1.0.7, any.lucid-svg ==0.7.1.1, - any.math-functions ==0.3.4.2, + any.math-functions ==0.3.4.3, math-functions +system-erf +system-expm1, + any.megaparsec ==9.6.1, + megaparsec -dev, any.memory ==0.18.0, memory +support_bytestring +support_deepseq, any.microlens ==0.4.13.1, - any.mime-types ==0.1.1.0, + any.mime-types ==0.1.2.0, any.mmorph ==1.2.0, + any.modern-uri ==0.3.6.1, + modern-uri -dev, any.monad-control ==1.0.3.1, any.monad-logger ==0.3.40, monad-logger +template_haskell, @@ -203,20 +243,30 @@ constraints: any.Cabal ==3.8.1.0, any.monad-time-effectful ==1.0.0.0, any.mono-traversable ==1.0.15.3, any.mtl ==2.2.2, + any.mtl-compat ==0.2.2, + mtl-compat -two-point-one -two-point-two, any.mwc-random ==0.15.0.2, + any.natural-arithmetic ==0.1.4.0, any.network ==3.1.4.0, network -devel, - any.network-byte-order ==0.1.6, + any.network-byte-order ==0.1.7, any.network-info ==0.2.1, any.network-uri ==2.6.4.2, any.odd-jobs ==0.2.3, any.old-locale ==1.0.0.7, any.old-time ==1.1.0.3, - any.optics-core ==0.4.1, + any.one-time-password ==3.0.0.0, + any.openapi3 ==3.2.4, + any.optics-core ==0.4.1.1, optics-core -explicit-generic-labels, + any.optics-extra ==0.4.2.1, + any.optics-th ==0.4.1, any.optparse-applicative ==0.18.1.0, optparse-applicative +process, + any.parallel ==3.2.2.0, any.parsec ==3.1.16.1, + any.parser-combinators ==1.3.0, + parser-combinators -dev, any.password ==3.0.2.1, password +argon2 +bcrypt +pbkdf2 +scrypt, any.password-types ==1.0.0.0, @@ -226,35 +276,47 @@ constraints: any.Cabal ==3.8.1.0, pg-entity -book -prod, any.pg-transact ==0.3.2.0, any.pg-transact-effectful ==0.0.1.0, - any.pipes ==4.3.16, - any.poolboy ==0.2.1.0, + any.poolboy ==0.2.2.0, any.postgresql-libpq ==0.9.5.0, postgresql-libpq -use-pkg-config, any.postgresql-migration ==0.2.1.7, - any.postgresql-simple ==0.6.5, + any.postgresql-simple ==0.6.5.1, any.pretty ==1.1.3.6, any.pretty-show ==1.10, any.prettyprinter ==1.7.1, prettyprinter -buildreadme +text, any.prettyprinter-ansi-terminal ==1.1.3, any.primitive ==0.8.0.0, - any.process ==1.6.16.0, + any.primitive-addr ==0.1.0.2, + any.primitive-offset ==0.2.0.0, + any.primitive-unlifted ==2.1.0.0, + any.process ==1.6.17.0, any.profunctors ==5.6.2, any.prometheus-client ==1.1.0, any.prometheus-metrics-ghc ==1.0.1.2, any.prometheus-proc ==0.1.5.0, - any.psqueues ==0.2.7.3, + any.psqueues ==0.2.8.0, + any.quickcheck-io ==0.2.0, any.random ==1.2.1.1, any.raven-haskell ==0.1.4.1, + raven-haskell -tests, any.recv ==0.1.0, + any.reflection ==2.1.7, + reflection -slow +template-haskell, any.regex-applicative ==0.3.4, + any.req ==3.13.1, + req -dev, any.resource-pool ==0.4.0.0, any.resourcet ==1.3.0, + any.retry ==0.9.3.1, + retry -lib-werror, any.rts ==1.0.2, + any.run-st ==0.1.3.2, any.safe ==0.3.19, - any.safe-exceptions ==0.1.7.3, + any.safe-exceptions ==0.1.7.4, any.scientific ==0.3.7.0, scientific -bytestring-builder -integer-simple, + any.sel ==0.0.1.0, any.semialign ==1.3, semialign +semigroupoids, any.semigroupoids ==5.3.7, @@ -267,6 +329,7 @@ constraints: any.Cabal ==3.8.1.0, any.servant-client-core ==0.19, any.servant-effectful ==0.0.1.0, any.servant-lucid ==0.9.0.6, + any.servant-openapi3 ==2.0.1.6, any.servant-server ==0.19.2, any.servant-static-th ==1.0.0.0, servant-static-th -buildexample, @@ -275,12 +338,12 @@ constraints: any.Cabal ==3.8.1.0, any.singleton-bool ==0.1.6, any.slugify ==0.1.0.1, any.socks ==0.6.1, - any.some ==1.0.5, + any.some ==1.0.6, some +newtype-unsafe, any.sop-core ==0.5.0.2, any.souffle-haskell ==3.5.1, - any.split ==0.2.3.5, - any.splitmix ==0.1.0.4, + any.split ==0.2.4, + any.splitmix ==0.1.0.5, splitmix -optimised-mixer, any.stm ==2.5.1.0, any.stm-chans ==3.0.0.9, @@ -294,39 +357,49 @@ constraints: any.Cabal ==3.8.1.0, any.string-conv ==0.2.0, string-conv -lib-werror, any.string-conversions ==0.4.0.1, - any.tagged ==0.8.7, + any.syb ==0.7.2.4, + any.tagged ==0.8.8, tagged +deepseq +transformers, - any.tasty ==1.4.3, + any.tar ==0.5.1.1, + tar -old-bytestring -old-time, + any.tasty ==1.5, tasty +unix, - any.tasty-hunit ==0.10.0.3, + any.tasty-hunit ==0.10.1, any.template-haskell ==2.19.0.0, any.temporary ==1.3, any.terminal-size ==0.3.4, any.terminfo ==0.4.1.5, any.text ==2.0.2, any.text-conversions ==0.3.1.1, - any.text-display ==0.0.5.0, + any.text-display ==0.0.5.1, text-display -book, any.text-manipulate ==0.3.1.0, any.text-short ==0.1.5, text-short -asserts, + any.tf-random ==0.5, any.th-abstraction ==0.5.0.0, any.th-compat ==0.1.4, + any.th-expand-syns ==0.4.11.0, + any.th-lift ==0.8.4, + any.th-orphans ==0.13.14, + any.th-reify-many ==0.1.10, any.these ==1.2, any.time ==1.12.2, any.time-compat ==1.9.6.1, time-compat -old-locale, - any.time-manager ==0.0.0, + any.time-manager ==0.0.1, any.timing-convenience ==0.1, - any.tls ==1.7.0, + any.tls ==1.9.0, tls +compat -hans +network, + any.torsor ==0.1, any.transformers ==0.5.6.2, any.transformers-base ==0.4.6, transformers-base +orphaninstances, any.transformers-compat ==0.7.2, transformers-compat -five +five-three -four +generic-deriving +mtl -three -two, + any.tuples ==0.1.0.0, any.type-equality ==1, - any.typed-process ==0.2.11.0, + any.typed-process ==0.2.11.1, any.unicode-data ==0.4.0.1, unicode-data -ucd2haskell, any.unicode-transforms ==0.4.0.1, @@ -335,17 +408,17 @@ constraints: any.Cabal ==3.8.1.0, any.unix-compat ==0.7, unix-compat -old-time, any.unix-memory ==0.1.2, - any.unix-time ==0.4.9, + any.unix-time ==0.4.11, any.unliftio ==0.2.25.0, any.unliftio-core ==0.2.1.0, any.unordered-containers ==0.2.19.1, unordered-containers -debug, any.utf8-string ==1.0.2, any.uuid ==1.3.15, - any.uuid-types ==1.0.5, + any.uuid-types ==1.0.5.1, any.vault ==0.3.1.5, vault +useghc, - any.vector ==0.13.0.0, + any.vector ==0.13.1.0, vector +boundschecks -internalchecks -unsafechecks -wall, any.vector-algorithms ==0.9.0.1, vector-algorithms +bench +boundschecks -internalchecks -llvm +properties -unsafechecks, @@ -353,21 +426,24 @@ constraints: any.Cabal ==3.8.1.0, any.void ==0.7.3, void -safe, any.wai ==3.2.3, - any.wai-app-static ==3.1.7.4, - wai-app-static +cryptonite -print, + any.wai-app-static ==3.1.8, + wai-app-static +crypton -print, any.wai-extra ==3.1.13.0, wai-extra -build-example, any.wai-log ==0.4.0.1, any.wai-logger ==2.4.0, any.wai-middleware-heartbeat ==0.0.1.0, any.wai-middleware-prometheus ==1.0.0.1, - any.warp ==3.3.28, + any.warp ==3.3.30, warp +allow-sendfilefd -network-bytestring -warp-debug -x509, + any.wide-word ==0.1.6.0, any.witherable ==0.4.2, any.wl-pprint-annotated ==0.1.0.1, any.word8 ==0.1.3, - any.xml-conduit ==1.9.1.2, - any.xml-conduit-writer ==0.1.1.2, + any.xml-conduit ==1.9.1.3, + any.xml-conduit-writer ==0.1.1.4, any.xml-types ==0.3.8, + any.zigzag ==0.0.1.0, any.zlib ==0.6.3.0, zlib -bundled-c-zlib -non-blocking-ffi -pkg-config +index-state: hackage.haskell.org 2023-11-22T07:29:39Z diff --git a/design/stories/alerts.stories.js b/design/stories/alerts.stories.js new file mode 100644 index 00000000..e4c582d5 --- /dev/null +++ b/design/stories/alerts.stories.js @@ -0,0 +1,6 @@ + +export default { + title: "Components/Alerts" +}; + +export const Alert = () => "

Info alert

Info alert

Error alert

Error alert!
" diff --git a/flora.cabal b/flora.cabal index 1e4e4b92..dd47b17b 100644 --- a/flora.cabal +++ b/flora.cabal @@ -12,7 +12,7 @@ extra-source-files: LICENSE README.md -tested-with: GHC ==9.4.5 +tested-with: GHC ==9.4.7 source-repository head type: git @@ -131,6 +131,7 @@ library Flora.Model.User.Query Flora.Model.User.Update Flora.Publish + Flora.QRCode Flora.Search JSON Log.Backend.File @@ -161,6 +162,7 @@ library , http-api-data , http-media , iso8601-time + , JuicyPixels , log-base , log-effectful , lucid @@ -168,6 +170,7 @@ library , monad-time-effectful , mtl , odd-jobs + , one-time-password , openapi3 , optics-core , password @@ -179,7 +182,10 @@ library , poolboy , postgresql-simple , pretty + , qrcode-core + , qrcode-juicypixels , resource-pool + , sel , servant , servant-lucid , servant-server @@ -213,6 +219,7 @@ library flora-web FloraWeb.API.Server.Packages FloraWeb.Client FloraWeb.Common.Auth + FloraWeb.Common.Auth.TwoFactor FloraWeb.Common.Auth.Types FloraWeb.Common.Guards FloraWeb.Common.Metrics @@ -220,6 +227,8 @@ library flora-web FloraWeb.Common.Pagination FloraWeb.Common.Tracing FloraWeb.Common.Utils + FloraWeb.Components.Alert + FloraWeb.Components.Button FloraWeb.Components.CategoryCard FloraWeb.Components.Footer FloraWeb.Components.Header @@ -240,12 +249,14 @@ library flora-web FloraWeb.Pages.Routes.Packages FloraWeb.Pages.Routes.Search FloraWeb.Pages.Routes.Sessions + FloraWeb.Pages.Routes.Settings FloraWeb.Pages.Server FloraWeb.Pages.Server.Admin FloraWeb.Pages.Server.Categories FloraWeb.Pages.Server.Packages FloraWeb.Pages.Server.Search FloraWeb.Pages.Server.Sessions + FloraWeb.Pages.Server.Settings FloraWeb.Pages.Templates FloraWeb.Pages.Templates.Admin FloraWeb.Pages.Templates.Admin.Packages @@ -253,13 +264,14 @@ library flora-web FloraWeb.Pages.Templates.Error FloraWeb.Pages.Templates.Haddock FloraWeb.Pages.Templates.Packages - FloraWeb.Pages.Templates.Pages.Categories - FloraWeb.Pages.Templates.Pages.Categories.Index - FloraWeb.Pages.Templates.Pages.Categories.Show - FloraWeb.Pages.Templates.Pages.Home - FloraWeb.Pages.Templates.Pages.Packages - FloraWeb.Pages.Templates.Pages.Search - FloraWeb.Pages.Templates.Pages.Sessions + FloraWeb.Pages.Templates.Screens.Categories + FloraWeb.Pages.Templates.Screens.Categories.Index + FloraWeb.Pages.Templates.Screens.Categories.Show + FloraWeb.Pages.Templates.Screens.Home + FloraWeb.Pages.Templates.Screens.Packages + FloraWeb.Pages.Templates.Screens.Search + FloraWeb.Pages.Templates.Screens.Sessions + FloraWeb.Pages.Templates.Screens.Settings FloraWeb.Pages.Templates.Types FloraWeb.Routes FloraWeb.Servant.Common @@ -271,8 +283,10 @@ library flora-web , aeson , async , base ^>=4.17 + , base32 , bytestring , Cabal-syntax + , chronos , clock , cmark-gfm , colourista @@ -302,6 +316,7 @@ library flora-web , mtl , network-uri , odd-jobs + , one-time-password , openapi3 , optics-core , password @@ -316,6 +331,7 @@ library flora-web , raven-haskell , resource-pool , safe-exceptions + , sel , servant , servant-client , servant-client-core @@ -325,6 +341,7 @@ library flora-web , text , text-display , time + , torsor , uuid , vector , vector-algorithms diff --git a/migrations/20231122154627_add_totp_to_users.sql b/migrations/20231122154627_add_totp_to_users.sql new file mode 100644 index 00000000..cb6fbb24 --- /dev/null +++ b/migrations/20231122154627_add_totp_to_users.sql @@ -0,0 +1,3 @@ +alter table users + add column totp_key text, + add column totp_enabled boolean not null; diff --git a/src/core/Flora/Model/User.hs b/src/core/Flora/Model/User.hs index 122ba828..8bf454c7 100644 --- a/src/core/Flora/Model/User.hs +++ b/src/core/Flora/Model/User.hs @@ -1,5 +1,5 @@ {-# LANGUAGE UndecidableInstances #-} -{-# OPTIONS_GHC -fno-warn-orphans -Wno-redundant-constraints #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} module Flora.Model.User ( UserId (..) @@ -34,12 +34,14 @@ import Database.PostgreSQL.Entity import Database.PostgreSQL.Entity.Types import Database.PostgreSQL.Simple.FromField (FromField (..), fromJSONField) import Database.PostgreSQL.Simple.FromRow (FromRow (..)) +import Database.PostgreSQL.Simple.Orphans () import Database.PostgreSQL.Simple.ToField (ToField (..), toJSONField) import Database.PostgreSQL.Simple.ToRow (ToRow (..)) import Effectful import Effectful.Time qualified as Time import GHC.Generics import GHC.TypeLits (ErrorMessage (..), TypeError) +import Sel.HMAC.SHA256 qualified as HMAC import Web.HttpApiData (FromHttpApiData, ToHttpApiData) newtype UserId = UserId {getUserId :: UUID} @@ -60,6 +62,8 @@ data User = User , userFlags :: UserFlags , createdAt :: UTCTime , updatedAt :: UTCTime + , totpKey :: Maybe HMAC.AuthenticationKey + , totpEnabled :: Bool } deriving stock (Eq, Generic, Show) deriving anyclass (FromRow, ToRow, NFData) @@ -125,6 +129,8 @@ mkUser UserCreationForm{username, email, password} = do let updatedAt = timestamp let displayName = "" let userFlags = UserFlags{isAdmin = False, canLogin = True} + let totpKey = Nothing + let totpEnabled = False pure User{..} mkAdmin :: IOE :> es => AdminCreationForm -> Eff es User @@ -135,6 +141,8 @@ mkAdmin AdminCreationForm{username, email, password} = do let updatedAt = timestamp let displayName = "" let userFlags = UserFlags{isAdmin = True, canLogin = False} + let totpKey = Nothing + let totpEnabled = False pure User{..} hashPassword :: IOE :> es => Password -> Eff es (PasswordHash Argon2) diff --git a/src/core/Flora/Model/User/Update.hs b/src/core/Flora/Model/User/Update.hs index a09ccb80..45d45c0e 100644 --- a/src/core/Flora/Model/User/Update.hs +++ b/src/core/Flora/Model/User/Update.hs @@ -1,40 +1,60 @@ {-# LANGUAGE QuasiQuotes #-} -module Flora.Model.User.Update where +module Flora.Model.User.Update + ( addAdmin + , lockAccount + , unlockAccount + , insertUser + , deleteUser + , setupTOTP + , confirmTOTP + , unSetTOTP + ) where import Control.Monad import Database.PostgreSQL.Entity (delete, insert) import Database.PostgreSQL.Entity.DBT (QueryNature (Update), execute) import Database.PostgreSQL.Simple (Only (Only)) import Database.PostgreSQL.Simple.SqlQQ (sql) - import Effectful (Eff, IOE, type (:>)) import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff) +import Effectful.Time (Time) +import Effectful.Time qualified as Time +import Sel.HMAC.SHA256 qualified as HMAC + import Flora.Model.User -addAdmin :: (DB :> es, IOE :> es) => AdminCreationForm -> Eff es User +addAdmin :: (DB :> es, Time :> es, IOE :> es) => AdminCreationForm -> Eff es User addAdmin form = do adminUser <- mkAdmin form insertUser adminUser - unlockAccount (adminUser.userId) + unlockAccount adminUser.userId pure adminUser -lockAccount :: DB :> es => UserId -> Eff es () -lockAccount userId = dbtToEff $ void $ execute Update q (Only userId) +lockAccount :: (DB :> es, Time :> es) => UserId -> Eff es () +lockAccount userId = do + ts <- Time.currentTime + dbtToEff $ void $ execute Update q (ts, userId) where q = [sql| - update users as u set user_flags = jsonb_set(user_flags, '{can_login}', 'false', false) + update users as u + set user_flags = jsonb_set(user_flags, '{can_login}', 'false', false), + updated_at = ? where u.user_id = ?; |] -unlockAccount :: DB :> es => UserId -> Eff es () -unlockAccount userId = dbtToEff $ void $ execute Update q (Only userId) +unlockAccount :: (DB :> es, Time :> es) => UserId -> Eff es () +unlockAccount userId = do + ts <- Time.currentTime + dbtToEff $ void $ execute Update q (ts, userId) where q = [sql| - update users as u set user_flags = jsonb_set(user_flags, '{can_login}', 'true', false) - where u.user_id = ?; + update users as u + set user_flags = jsonb_set(user_flags, '{can_login}', 'true', false), + updated_at = ? + where u.user_id = ? |] insertUser :: DB :> es => User -> Eff es () @@ -42,3 +62,53 @@ insertUser user = dbtToEff $ insert @User user deleteUser :: DB :> es => UserId -> Eff es () deleteUser userId = dbtToEff $ delete @User (Only userId) + +setupTOTP + :: (DB :> es, Time :> es) + => UserId + -> HMAC.AuthenticationKey + -> Eff es () +setupTOTP userId key = do + ts <- Time.currentTime + dbtToEff $ void $ execute Update q (key, ts, userId) + where + q = + [sql| + update users as u + set totp_key = ?, + updated_at = ? + where u.user_id = ?; + |] + +confirmTOTP + :: (DB :> es, Time :> es) + => UserId + -> Eff es () +confirmTOTP userId = do + ts <- Time.currentTime + dbtToEff $ void $ execute Update q (ts, userId) + where + q = + [sql| + update users as u + set totp_enabled = true, + updated_at = ? + where u.user_id = ?; + |] + +unSetTOTP + :: (DB :> es, Time :> es) + => UserId + -> Eff es () +unSetTOTP userId = do + ts <- Time.currentTime + dbtToEff $ void $ execute Update q (ts, userId) + where + q = + [sql| + update users as u + set totp_enabled = false, + totp_key = Null, + updated_at = ? + where u.user_id = ?; + |] diff --git a/src/core/Flora/QRCode.hs b/src/core/Flora/QRCode.hs new file mode 100644 index 00000000..9849fc20 --- /dev/null +++ b/src/core/Flora/QRCode.hs @@ -0,0 +1,20 @@ +module Flora.QRCode where + +import Codec.Picture +import Codec.QRCode +import Codec.QRCode.JuicyPixels +import Data.ByteString (StrictByteString) +import Data.ByteString.Base64 +import Data.ByteString.Lazy qualified as BSL +import Data.Function ((&)) +import Data.Text (Text) + +generateQRCode :: Text -> StrictByteString +generateQRCode uri = + case encodeText (defaultQRCodeOptions L) Iso8859_1OrUtf8WithoutECI uri of + Nothing -> error $ "QR code can't be encoded for text " <> show uri + Just qrImage -> + toImage 4 20 qrImage + & encodePng + & BSL.toStrict + & encodeBase64' diff --git a/src/datatypes/Database/PostgreSQL/Simple/Orphans.hs b/src/datatypes/Database/PostgreSQL/Simple/Orphans.hs index 2df5e805..c09a7d39 100644 --- a/src/datatypes/Database/PostgreSQL/Simple/Orphans.hs +++ b/src/datatypes/Database/PostgreSQL/Simple/Orphans.hs @@ -4,6 +4,24 @@ module Database.PostgreSQL.Simple.Orphans where import Control.DeepSeq import Data.ByteString (ByteString) -import Database.PostgreSQL.Simple.Types +import Data.Text qualified as Text +import Database.PostgreSQL.Simple.FromField (FromField (..), ResultError (..), returnError) +import Database.PostgreSQL.Simple.ToField +import Database.PostgreSQL.Simple.Types (Binary (..)) +import Sel.HMAC.SHA256 qualified as HMAC deriving newtype instance NFData (Binary ByteString) + +instance FromField HMAC.AuthenticationKey where + fromField f Nothing = returnError UnexpectedNull f "" + fromField f (Just bs) = + case HMAC.authenticationKeyFromHexByteString bs of + Left err -> returnError ConversionFailed f (Text.unpack err) + Right value -> pure value + +instance ToField HMAC.AuthenticationKey where + toField = Escape . HMAC.unsafeAuthenticationKeyToHexByteString + +instance NFData HMAC.AuthenticationKey where + rnf :: HMAC.AuthenticationKey -> () + rnf a = seq a () diff --git a/src/web/FloraWeb/Common/Auth.hs b/src/web/FloraWeb/Common/Auth.hs index 4637003a..73830461 100644 --- a/src/web/FloraWeb/Common/Auth.hs +++ b/src/web/FloraWeb/Common/Auth.hs @@ -1,7 +1,9 @@ module FloraWeb.Common.Auth ( module FloraWeb.Common.Auth.Types - , FloraAuthContext - , authHandler + , OptionalAuthContext + , StrictAuthContext + , optionalAuthHandler + , strictAuthHandler ) where @@ -37,36 +39,56 @@ import FloraWeb.Session import FloraWeb.Types import Servant qualified -type FloraAuthContext = AuthHandler Request (Headers '[Header "Set-Cookie" SetCookie] Session) +type OptionalAuthContext = AuthHandler Request (Headers '[Header "Set-Cookie" SetCookie] Session) +type StrictAuthContext = AuthHandler Request (Headers '[Header "Set-Cookie" SetCookie] Session) -authHandler :: Logger -> FloraEnv -> FloraAuthContext -authHandler logger floraEnv = +optionalAuthHandler :: Logger -> FloraEnv -> OptionalAuthContext +optionalAuthHandler logger floraEnv = mkAuthHandler ( \request -> - handler request - & Logging.runLog (floraEnv.environment) logger - & DB.runDB (floraEnv.pool) + handler False floraEnv request + & Logging.runLog floraEnv.environment logger + & DB.runDB floraEnv.pool & runVisitorSession & effToHandler ) - where - handler :: Request -> Eff '[Log, DB, IsVisitor, Error ServerError, IOE] (Headers '[Header "Set-Cookie" SetCookie] Session) - handler req = do - let cookies = getCookies req - mbPersistentSessionId <- handlerToEff $ getSessionId cookies - mbPersistentSession <- getInTheFuckingSessionShinji mbPersistentSessionId - mUserInfo <- fetchUser mbPersistentSession - requestID <- liftIO $ getRequestID req - (mUser, sessionId) <- do - case mUserInfo of - Nothing -> do + +strictAuthHandler :: Logger -> FloraEnv -> StrictAuthContext +strictAuthHandler logger floraEnv = + mkAuthHandler + ( \request -> + handler True floraEnv request + & Logging.runLog floraEnv.environment logger + & DB.runDB floraEnv.pool + & runVisitorSession + & effToHandler + ) + +handler + :: Bool + -> FloraEnv + -> Request + -> Eff + '[Log, DB, IsVisitor, Error ServerError, IOE] + (Headers '[Header "Set-Cookie" SetCookie] Session) +handler mustBeConnected floraEnv req = do + let cookies = getCookies req + mbPersistentSessionId <- handlerToEff $ getSessionId cookies + mbPersistentSession <- getInTheFuckingSessionShinji mbPersistentSessionId + mUserInfo <- fetchUser mbPersistentSession + requestID <- liftIO $ getRequestID req + (mUser, sessionId) <- do + case mUserInfo of + Nothing -> + if mustBeConnected + then throwError $ err401{errBody = "Connect first"} + else do nSessionId <- liftIO newPersistentSessionId pure (Nothing, nSessionId) - Just (user, userSession) -> do - pure (Just user, userSession.persistentSessionId) - webEnvStore <- liftIO $ newWebEnvStore (WebEnv floraEnv) - let sessionCookie = craftSessionCookie sessionId False - pure $ addCookie sessionCookie (Session{..}) + Just (user, userSession) -> pure (Just user, userSession.persistentSessionId) + webEnvStore <- liftIO $ newWebEnvStore (WebEnv floraEnv) + let sessionCookie = craftSessionCookie sessionId False + pure $ addCookie sessionCookie (Session{..}) getCookies :: Request -> Cookies getCookies req = @@ -101,10 +123,13 @@ getInTheFuckingSessionShinji (Just persistentSessionId) = do Nothing -> pure Nothing (Just userSession) -> pure (Just userSession) -fetchUser :: (Error ServerError :> es, DB :> es) => Maybe PersistentSession -> Eff es (Maybe (User, PersistentSession)) +fetchUser + :: (Error ServerError :> es, DB :> es) + => Maybe PersistentSession + -> Eff es (Maybe (User, PersistentSession)) fetchUser Nothing = pure Nothing fetchUser (Just userSession) = do - user <- lookupUser (userSession.userId) + user <- lookupUser userSession.userId pure (Just (user, userSession)) lookupUser :: (Error ServerError :> es, DB :> es) => UserId -> Eff es User @@ -119,8 +144,8 @@ handlerToEff . Error ServerError :> es => Handler a -> Eff es a -handlerToEff handler = do - v <- unsafeEff_ $ Servant.runHandler handler +handlerToEff handler' = do + v <- unsafeEff_ $ Servant.runHandler handler' either throwError pure v effToHandler diff --git a/src/web/FloraWeb/Common/Auth/TwoFactor.hs b/src/web/FloraWeb/Common/Auth/TwoFactor.hs new file mode 100644 index 00000000..30bcab89 --- /dev/null +++ b/src/web/FloraWeb/Common/Auth/TwoFactor.hs @@ -0,0 +1,44 @@ +module FloraWeb.Common.Auth.TwoFactor + ( uriFromKey + , validateTOTP + ) where + +import Chronos (Timespan, now, second) +import Data.ByteString.Base32 qualified as Base32 +import Data.Maybe (fromJust) +import Data.Text (Text) +import OTP.Commons +import OTP.TOTP +import Sel.HMAC.SHA256 qualified as HMAC +import Torsor (scale) + +period :: Timespan +period = scale 30 second + +sixDigits :: Digits +sixDigits = fromJust $ mkDigits 6 + +uriFromKey :: Text -> Text -> HMAC.AuthenticationKey -> Text +uriFromKey domain email key = + let + issuer = "Flora (" <> domain <> ")" + in + totpToURI + (Base32.encodeBase32Unpadded $ HMAC.unsafeAuthenticationKeyToBinary key) + email + issuer + sixDigits + period + HMAC_SHA1 + +validateTOTP :: HMAC.AuthenticationKey -> Text -> IO Bool +validateTOTP key code = do + timestamp <- now + pure $ + totpSHA1Check + key + (1, 1) + timestamp + period + sixDigits + code diff --git a/src/web/FloraWeb/Common/Auth/Types.hs b/src/web/FloraWeb/Common/Auth/Types.hs index b2325653..72c811a6 100644 --- a/src/web/FloraWeb/Common/Auth/Types.hs +++ b/src/web/FloraWeb/Common/Auth/Types.hs @@ -68,33 +68,24 @@ demoteSession -> Eff (IsVisitor : es) a demoteSession = putVisitorTag . runAdminSession +type BaseEffects = + '[ DB + , Time + , Reader (Headers '[Header "Set-Cookie" SetCookie] Session) + , Reader FeatureEnv + , BlobStoreAPI + , Log + , Error ServerError + , IOE + ] + -- | Datatypes used for every route that doesn't *need* an authenticated user type FloraPage = - Eff - '[ IsVisitor - , DB - , Time - , Reader (Headers '[Header "Set-Cookie" SetCookie] Session) - , Reader FeatureEnv - , BlobStoreAPI - , Log - , Error ServerError - , IOE - ] + Eff (IsVisitor ': BaseEffects) -- | Datatypes used for routes that *need* an admin type FloraAdmin = - Eff - '[ IsAdmin - , DB - , Time - , Reader (Headers '[Header "Set-Cookie" SetCookie] Session) - , Reader FeatureEnv - , BlobStoreAPI - , Log - , Error ServerError - , IOE - ] + Eff (IsAdmin ': BaseEffects) -- | The effect stack for the development websockets type FloraDevSocket = Eff [Reader (), Log, Error ServerError, IOE] @@ -103,6 +94,6 @@ type instance AuthServerData (AuthProtect "optional-cookie-auth") = (Headers '[Header "Set-Cookie" SetCookie] Session) --- type instance --- AuthServerData (AuthProtect "cookie-auth") = --- (Headers '[Header "Set-Cookie" SetCookie] (Session 'Authenticated)) +type instance + AuthServerData (AuthProtect "cookie-auth") = + (Headers '[Header "Set-Cookie" SetCookie] Session) diff --git a/src/web/FloraWeb/Common/Guards.hs b/src/web/FloraWeb/Common/Guards.hs index 5ca15720..2eb3902b 100644 --- a/src/web/FloraWeb/Common/Guards.hs +++ b/src/web/FloraWeb/Common/Guards.hs @@ -2,17 +2,28 @@ module FloraWeb.Common.Guards where +import Data.Text (Text) import Distribution.Types.Version (Version) import Effectful import Effectful.Log (Log) import Effectful.PostgreSQL.Transact.Effect import Effectful.Time (Time) +import FloraWeb.Pages.Templates +import Log qualified +import Optics.Core +import Servant (respond) +import Servant.API.UVerb + import Flora.Model.Package import Flora.Model.Package.Query qualified as Query import Flora.Model.PackageIndex.Query as Query import Flora.Model.PackageIndex.Types (PackageIndex) import Flora.Model.Release.Query qualified as Query import Flora.Model.Release.Types (Release) +import FloraWeb.Common.Auth +import FloraWeb.Pages.Routes.Sessions (CreateSessionResponses) +import FloraWeb.Pages.Templates.Screens.Sessions qualified as Sessions +import FloraWeb.Session (getSession) guardThatPackageExists :: (DB :> es, Log :> es, Time :> es) @@ -54,3 +65,19 @@ guardThatPackageIndexExists namespace action = do case result of Just packageIndex -> pure packageIndex Nothing -> action namespace + +guardThatUserHasProvidedTOTP + :: Maybe Text + -> (Text -> FloraPage (Union CreateSessionResponses)) + -> FloraPage (Union CreateSessionResponses) +guardThatUserHasProvidedTOTP mTOTP action = do + case mTOTP of + Just totp -> action totp + Nothing -> do + session <- getSession + Log.logInfo_ "User did not provide a TOTP code" + templateDefaults <- fromSession session defaultTemplateEnv + let templateEnv = + templateDefaults + & (#flashError ?~ mkError "Must provide an OTP code") + respond $ WithStatus @401 $ renderUVerb templateEnv Sessions.newSession diff --git a/src/web/FloraWeb/Components/Alert.hs b/src/web/FloraWeb/Components/Alert.hs new file mode 100644 index 00000000..b240a1ca --- /dev/null +++ b/src/web/FloraWeb/Components/Alert.hs @@ -0,0 +1,20 @@ +module FloraWeb.Components.Alert where + +import Data.Text (Text) +import FloraWeb.Components.Icons qualified as Icons +import FloraWeb.Pages.Templates.Types +import Lucid + +info :: Text -> FloraHTML +info message = + output_ [role_ "status", class_ "alert alert-info"] $ do + Icons.information + div_ [class_ "alert-message"] $ + toHtml message + +exception :: Text -> FloraHTML +exception message = + output_ [role_ "status", class_ "alert alert-error"] $ do + Icons.exception + div_ [class_ "alert-message"] $ + toHtml message diff --git a/src/web/FloraWeb/Components/Button.hs b/src/web/FloraWeb/Components/Button.hs new file mode 100644 index 00000000..8f78457c --- /dev/null +++ b/src/web/FloraWeb/Components/Button.hs @@ -0,0 +1,8 @@ +module FloraWeb.Components.Button where + +import FloraWeb.Pages.Templates +import Lucid + +button :: FloraHTML -> FloraHTML +button text = + button_ [type_ "submit", class_ "button"] text diff --git a/src/web/FloraWeb/Components/Icons.hs b/src/web/FloraWeb/Components/Icons.hs index 0b4a4847..35e34cd6 100644 --- a/src/web/FloraWeb/Components/Icons.hs +++ b/src/web/FloraWeb/Components/Icons.hs @@ -5,6 +5,8 @@ module FloraWeb.Components.Icons , chevronRightOutline , pen , lookingGlass + , information + , exception ) where import Data.Text (Text) @@ -55,3 +57,21 @@ lookingGlass = button_ [type_ "submit"] $ svg_ [xmlns_ "http://www.w3.org/2000/svg", style_ "color: gray", fill_ "none", viewBox_ "0 0 24 24", stroke_ "currentColor"] $ path_ [stroke_linecap_ "round", stroke_linejoin_ "round", stroke_width_ "2", d_ "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"] + +information :: FloraHTML +information = + toHtmlRaw @Text + [str| + + + + |] + +exception :: FloraHTML +exception = + toHtmlRaw @Text + [str| + + + + |] diff --git a/src/web/FloraWeb/Components/Navbar.hs b/src/web/FloraWeb/Components/Navbar.hs index b32b7bcd..b7e8ea62 100644 --- a/src/web/FloraWeb/Components/Navbar.hs +++ b/src/web/FloraWeb/Components/Navbar.hs @@ -28,7 +28,7 @@ navbar = do navBarLink' "/about" "About" aboutNav navBarLink' "/categories" "Categories" packagesNav navBarLink' "/packages" "Packages" packagesNav - -- userMenu + userMenu themeToggle brand :: FloraHTML diff --git a/src/web/FloraWeb/Pages/Routes.hs b/src/web/FloraWeb/Pages/Routes.hs index e4c16263..703a5ade 100644 --- a/src/web/FloraWeb/Pages/Routes.hs +++ b/src/web/FloraWeb/Pages/Routes.hs @@ -5,6 +5,7 @@ import FloraWeb.Pages.Routes.Categories qualified as Categories import FloraWeb.Pages.Routes.Packages qualified as Packages import FloraWeb.Pages.Routes.Search qualified as Search import FloraWeb.Pages.Routes.Sessions qualified as Sessions +import FloraWeb.Pages.Routes.Settings qualified as Settings import Lucid import Servant import Servant.API.Generic @@ -20,6 +21,7 @@ data Routes' mode = Routes' , packages :: mode :- "packages" :> Packages.Routes , categories :: mode :- "categories" :> Categories.Routes , search :: mode :- "search" :> Search.Routes + , settings :: mode :- AuthProtect "cookie-auth" :> "settings" :> Settings.Routes , notFound :: mode :- Get '[HTML] (Html ()) } deriving stock (Generic) diff --git a/src/web/FloraWeb/Pages/Routes/Sessions.hs b/src/web/FloraWeb/Pages/Routes/Sessions.hs index f517336e..4d67b46f 100644 --- a/src/web/FloraWeb/Pages/Routes/Sessions.hs +++ b/src/web/FloraWeb/Pages/Routes/Sessions.hs @@ -54,7 +54,7 @@ data Routes' mode = Routes' data LoginForm = LoginForm { email :: Text , password :: Text - , remember :: Maybe () + , totp :: Maybe Text } deriving stock (Generic) diff --git a/src/web/FloraWeb/Pages/Routes/Settings.hs b/src/web/FloraWeb/Pages/Routes/Settings.hs new file mode 100644 index 00000000..ad4033ea --- /dev/null +++ b/src/web/FloraWeb/Pages/Routes/Settings.hs @@ -0,0 +1,68 @@ +module FloraWeb.Pages.Routes.Settings + ( Routes + , Routes' (..) + , TwoFactorSetupResponses + , TwoFactorConfirmationForm (..) + , DeleteTwoFactorSetupResponse + ) +where + +import Lucid +import Servant +import Servant.API.Generic +import Servant.HTML.Lucid + +import Data.Text (Text) +import FloraWeb.Common.Auth () +import Web.FormUrlEncoded + +type Routes = + NamedRoutes Routes' + +type GetUserSettings = + Get '[HTML] (Html ()) + +type GetUserSecuritySettings = + "security" + :> Get '[HTML] (Html ()) + +type GetTwoFactorSettingsPage = + "security" + :> "two-factor" + :> Get '[HTML] (Html ()) + +type TwoFactorSetupResponses = + '[ WithStatus 200 (Html ()) + , WithStatus 301 (Headers '[Header "Location" Text] NoContent) + ] + +data TwoFactorConfirmationForm = TwoFactorConfirmationForm + { code :: Text + } + deriving stock (Generic) + deriving anyclass (FromForm, ToForm) + +type PostTwoFactorSetup = + "security" + :> "two-factor" + :> "setup" + :> ReqBody '[FormUrlEncoded] TwoFactorConfirmationForm + :> UVerb 'POST '[HTML] TwoFactorSetupResponses + +type DeleteTwoFactorSetup = + "security" + :> "two-factor" + :> "delete" + :> Verb 'POST 301 '[HTML] DeleteTwoFactorSetupResponse + +type DeleteTwoFactorSetupResponse = + Headers '[Header "Location" Text] NoContent + +data Routes' mode = Routes' + { index :: mode :- GetUserSettings + , getSecuritySettings :: mode :- GetUserSecuritySettings + , getTwoFactorSettings :: mode :- GetTwoFactorSettingsPage + , postTwoFactorSetup :: mode :- PostTwoFactorSetup + , deleteTwoFactorSetup :: mode :- DeleteTwoFactorSetup + } + deriving stock (Generic) diff --git a/src/web/FloraWeb/Pages/Server.hs b/src/web/FloraWeb/Pages/Server.hs index 7f33ad22..f003b739 100644 --- a/src/web/FloraWeb/Pages/Server.hs +++ b/src/web/FloraWeb/Pages/Server.hs @@ -13,9 +13,10 @@ import FloraWeb.Pages.Server.Categories qualified as Categories import FloraWeb.Pages.Server.Packages qualified as Packages import FloraWeb.Pages.Server.Search qualified as Search import FloraWeb.Pages.Server.Sessions qualified as Sessions +import FloraWeb.Pages.Server.Settings qualified as Settings import FloraWeb.Pages.Templates import FloraWeb.Pages.Templates.Error (web404) -import FloraWeb.Pages.Templates.Pages.Home qualified as Home +import FloraWeb.Pages.Templates.Screens.Home qualified as Home import FloraWeb.Session import OddJobs.Endpoints qualified as OddJobs import OddJobs.Types qualified as OddJobs @@ -30,6 +31,7 @@ server cfg env = , packages = Packages.server , categories = Categories.server , search = Search.server + , settings = \_ -> hoistServerWithContext (Proxy @Settings.Routes) (Proxy @'[OptionalAuthContext]) id Settings.server , notFound = serveNotFound } diff --git a/src/web/FloraWeb/Pages/Server/Categories.hs b/src/web/FloraWeb/Pages/Server/Categories.hs index ecbc4e28..a50f6f18 100644 --- a/src/web/FloraWeb/Pages/Server/Categories.hs +++ b/src/web/FloraWeb/Pages/Server/Categories.hs @@ -12,7 +12,7 @@ import FloraWeb.Common.Auth import FloraWeb.Pages.Routes.Categories import FloraWeb.Pages.Templates (defaultTemplateEnv, fromSession, render) import FloraWeb.Pages.Templates.Error -import FloraWeb.Pages.Templates.Pages.Categories qualified as Template +import FloraWeb.Pages.Templates.Screens.Categories qualified as Template import FloraWeb.Session (getSession) server :: ServerT Routes FloraPage diff --git a/src/web/FloraWeb/Pages/Server/Packages.hs b/src/web/FloraWeb/Pages/Server/Packages.hs index 259d436c..01094d95 100644 --- a/src/web/FloraWeb/Pages/Server/Packages.hs +++ b/src/web/FloraWeb/Pages/Server/Packages.hs @@ -41,8 +41,8 @@ import FloraWeb.Pages.Routes.Packages import FloraWeb.Pages.Templates import FloraWeb.Pages.Templates.Error import FloraWeb.Pages.Templates.Packages qualified as Package -import FloraWeb.Pages.Templates.Pages.Packages qualified as Packages -import FloraWeb.Pages.Templates.Pages.Search qualified as Search +import FloraWeb.Pages.Templates.Screens.Packages qualified as Packages +import FloraWeb.Pages.Templates.Screens.Search qualified as Search import FloraWeb.Session server :: ServerT Routes FloraPage @@ -272,7 +272,7 @@ constructTarballPath pname v = display pname <> "-" <> display v <> ".tar.gz" getTarballHandler :: Namespace -> PackageName -> Version -> Text -> FloraPage ByteString getTarballHandler namespace packageName version tarballName = do features <- ask @FeatureEnv - unless (isJust $ features.blobStoreImpl) $! throwError err404 + unless (isJust features.blobStoreImpl) $ throwError err404 package <- guardThatPackageExists namespace packageName $ \_ _ -> web404 release <- guardThatReleaseExists package.packageId version $ const web404 case release.tarballRootHash of diff --git a/src/web/FloraWeb/Pages/Server/Search.hs b/src/web/FloraWeb/Pages/Server/Search.hs index 18612442..244dae95 100644 --- a/src/web/FloraWeb/Pages/Server/Search.hs +++ b/src/web/FloraWeb/Pages/Server/Search.hs @@ -12,7 +12,7 @@ import Flora.Search qualified as Search import FloraWeb.Common.Pagination import FloraWeb.Pages.Routes.Search (Routes, Routes' (..)) import FloraWeb.Pages.Templates (TemplateEnv (..), defaultTemplateEnv, fromSession, render) -import FloraWeb.Pages.Templates.Pages.Search qualified as Search +import FloraWeb.Pages.Templates.Screens.Search qualified as Search import FloraWeb.Session server :: ServerT Routes FloraPage diff --git a/src/web/FloraWeb/Pages/Server/Sessions.hs b/src/web/FloraWeb/Pages/Server/Sessions.hs index e1714946..ba922cc8 100644 --- a/src/web/FloraWeb/Pages/Server/Sessions.hs +++ b/src/web/FloraWeb/Pages/Server/Sessions.hs @@ -1,21 +1,28 @@ +{-# LANGUAGE OverloadedRecordDot #-} + module FloraWeb.Pages.Server.Sessions where +import Data.Maybe import Data.Password.Argon2 import Data.Text.Display import Log qualified import Optics.Core +import Servant +import Control.Monad.IO.Class +import Data.Text (Text) import Flora.Model.PersistentSession import Flora.Model.User import Flora.Model.User.Orphans () import Flora.Model.User.Query qualified as Query import FloraWeb.Common.Auth +import FloraWeb.Common.Auth.TwoFactor qualified as TwoFactor +import FloraWeb.Common.Guards (guardThatUserHasProvidedTOTP) import FloraWeb.Common.Utils import FloraWeb.Pages.Routes.Sessions import FloraWeb.Pages.Templates -import FloraWeb.Pages.Templates.Pages.Sessions as Sessions +import FloraWeb.Pages.Templates.Screens.Sessions as Sessions import FloraWeb.Session -import Servant server :: ServerT Routes FloraPage server = @@ -39,7 +46,7 @@ newSessionHandler = do respond $ WithStatus @301 (redirect "/") createSessionHandler :: LoginForm -> FloraPage (Union CreateSessionResponses) -createSessionHandler LoginForm{email, password} = do +createSessionHandler LoginForm{email, password, totp} = do session <- getSession mUser <- Query.getUserByEmail email case mUser of @@ -51,12 +58,16 @@ createSessionHandler LoginForm{email, password} = do & (#flashError ?~ mkError "Could not authenticate") respond $ WithStatus @401 $ renderUVerb templateEnv Sessions.newSession Just user -> - if validatePassword (mkPassword password) (user.password) + if validatePassword (mkPassword password) user.password then do - Log.logInfo_ "[+] User connected!" - sessionId <- persistSession (session.sessionId) (user.userId) - let sessionCookie = craftSessionCookie sessionId True - respond $ WithStatus @301 $ redirectWithCookie "/" sessionCookie + if user.totpEnabled + then guardThatUserHasProvidedTOTP totp $ \userCode -> do + checkTOTPIsValid userCode user + else do + Log.logInfo_ "[+] User connected!" + sessionId <- persistSession session.sessionId user.userId + let sessionCookie = craftSessionCookie sessionId True + respond $ WithStatus @301 $ redirectWithCookie "/" sessionCookie else do Log.logInfo_ "[+] Couldn't authenticate user" templateDefaults <- fromSession session defaultTemplateEnv @@ -65,6 +76,27 @@ createSessionHandler LoginForm{email, password} = do & (#flashError ?~ mkError "Could not authenticate") respond $ WithStatus @401 $ renderUVerb templateEnv Sessions.newSession +checkTOTPIsValid + :: Text + -> User + -> FloraPage (Union CreateSessionResponses) +checkTOTPIsValid userCode user = do + session <- getSession + validated <- liftIO $ TwoFactor.validateTOTP (fromJust user.totpKey) userCode + if validated + then do + Log.logInfo_ "[+] User connected!" + sessionId <- persistSession session.sessionId user.userId + let sessionCookie = craftSessionCookie sessionId True + respond $ WithStatus @301 $ redirectWithCookie "/" sessionCookie + else do + Log.logInfo_ "[+] Couldn't authenticate user's TOTP code" + templateDefaults <- fromSession session defaultTemplateEnv + let templateEnv = + templateDefaults + & (#flashError ?~ mkError "Could not authenticate") + respond $ WithStatus @401 $ renderUVerb templateEnv Sessions.newSession + deleteSessionHandler :: PersistentSessionId -> FloraPage DeleteSessionResponse deleteSessionHandler sessionId = do Log.logInfo_ $ "[+] Logging-off session " <> display sessionId diff --git a/src/web/FloraWeb/Pages/Server/Settings.hs b/src/web/FloraWeb/Pages/Server/Settings.hs new file mode 100644 index 00000000..cd500eff --- /dev/null +++ b/src/web/FloraWeb/Pages/Server/Settings.hs @@ -0,0 +1,130 @@ +module FloraWeb.Pages.Server.Settings + ( Routes + , server + ) where + +import Control.Monad.IO.Class +import Data.ByteString.Base32 qualified as Base32 +import Data.Maybe (fromJust) +import Data.Text.Encoding qualified as Text +import Log qualified +import Lucid +import Optics.Core +import Sel.HMAC.SHA256 qualified as HMAC +import Servant + +import Flora.Environment +import Flora.Model.User +import Flora.Model.User.Update qualified as Update +import Flora.QRCode qualified as QRCode +import FloraWeb.Common.Auth.TwoFactor qualified as TwoFactor +import FloraWeb.Common.Utils (redirect) +import FloraWeb.Pages.Routes.Settings +import FloraWeb.Pages.Templates (render, renderUVerb) +import FloraWeb.Pages.Templates.Screens.Settings qualified as Settings +import FloraWeb.Pages.Templates.Types +import FloraWeb.Session + +server :: ServerT Routes FloraPage +server = + Routes' + { index = userSettingsHandler + , getSecuritySettings = userSecuritySettingsHandler + , getTwoFactorSettings = getTwoFactorSettingsHandler + , postTwoFactorSetup = postTwoFactorSetupHandler + , deleteTwoFactorSetup = deleteTwoFactorSetupHandler + } + +userSettingsHandler :: FloraPage (Html ()) +userSettingsHandler = do + session <- getSession + templateEnv' <- fromSession session defaultTemplateEnv + let templateEnv = + templateEnv' + & #title .~ "Account settings" + let user = fromJust session.mUser + render templateEnv $ + Settings.dashboard user + +userSecuritySettingsHandler :: FloraPage (Html ()) +userSecuritySettingsHandler = do + session <- getSession + templateEnv' <- fromSession session defaultTemplateEnv + let templateEnv = + templateEnv' + & #title .~ "Security settings" + render + templateEnv + Settings.securitySettings + +getTwoFactorSettingsHandler :: FloraPage (Html ()) +getTwoFactorSettingsHandler = do + FloraEnv{domain} <- getEnv + session <- getSession + templateEnv' <- fromSession session defaultTemplateEnv + let templateEnv = + templateEnv' + & #title .~ "Security settings" + let user = fromJust session.mUser + case user.totpKey of + Nothing -> do + userKey <- liftIO HMAC.newAuthenticationKey + Update.setupTOTP user.userId userKey + let uri = TwoFactor.uriFromKey domain user.email userKey + let qrCode = + QRCode.generateQRCode uri + & Text.decodeUtf8 + render templateEnv $ + Settings.twoFactorSettings + qrCode + (Base32.encodeBase32Unpadded $ HMAC.unsafeAuthenticationKeyToBinary userKey) + Just userKey -> do + if user.totpEnabled + then render templateEnv Settings.twoFactorSettingsRemove + else do + let uri = TwoFactor.uriFromKey domain user.email userKey + let qrCode = + QRCode.generateQRCode uri + & Text.decodeUtf8 + render templateEnv $ + Settings.twoFactorSettings + qrCode + (Base32.encodeBase32Unpadded $ HMAC.unsafeAuthenticationKeyToBinary userKey) + +postTwoFactorSetupHandler :: TwoFactorConfirmationForm -> FloraPage (Union TwoFactorSetupResponses) +postTwoFactorSetupHandler TwoFactorConfirmationForm{code = userCode} = do + session <- getSession + templateEnv' <- fromSession session defaultTemplateEnv + let user = fromJust session.mUser + case user.totpKey of + Nothing -> respond $ WithStatus @301 (redirect "/settings/security/two-factor") + Just userKey -> do + validated <- liftIO $ TwoFactor.validateTOTP userKey userCode + if validated + then do + Update.confirmTOTP user.userId + Log.logInfo_ "Code validation succeeded" + respond $ WithStatus @301 (redirect "/settings/security/two-factor") + else do + Log.logAttention_ "Code validation failed" + let templateEnv = + templateEnv' + & #title .~ "Security settings" + & #flashError ?~ mkError "Code validation failed, please retry" + let uri = TwoFactor.uriFromKey "localhost" user.email userKey + let qrCode = + QRCode.generateQRCode uri + & Text.decodeUtf8 + respond $ + WithStatus @200 $ + renderUVerb templateEnv $ + Settings.twoFactorSettings + qrCode + (Base32.encodeBase32Unpadded $ HMAC.unsafeAuthenticationKeyToBinary userKey) + +deleteTwoFactorSetupHandler :: FloraPage DeleteTwoFactorSetupResponse +deleteTwoFactorSetupHandler = do + session <- getSession + let user = fromJust session.mUser + Update.unSetTOTP user.userId + pure $ redirect "/settings/security" diff --git a/src/web/FloraWeb/Pages/Templates.hs b/src/web/FloraWeb/Pages/Templates.hs index df0b79cd..e8c61cdf 100644 --- a/src/web/FloraWeb/Pages/Templates.hs +++ b/src/web/FloraWeb/Pages/Templates.hs @@ -6,12 +6,15 @@ module FloraWeb.Pages.Templates ) where +import Control.Monad.Extra (whenJust) import Control.Monad.Identity (runIdentity) -import Control.Monad.Reader (runReaderT) +import Control.Monad.Reader (ask, runReaderT) import Data.ByteString.Lazy +import Data.Text.Display import Lucid import Flora.Environment (DeploymentEnv (..)) +import FloraWeb.Components.Alert qualified as Alert import FloraWeb.Components.Header (header) import FloraWeb.Pages.Templates.Types as Types @@ -30,7 +33,12 @@ mkErrorPage env template = rendered :: DeploymentEnv -> FloraHTML -> FloraHTML rendered _deploymentEnv target = do + TemplateEnv{flashInfo, flashError} <- ask header + whenJust flashInfo $ \msg -> do + Alert.info (display msg) + whenJust flashError $ \msg -> do + Alert.exception (display msg) main_ [] target -- when (deploymentEnv == Development) $ diff --git a/src/web/FloraWeb/Pages/Templates/Packages.hs b/src/web/FloraWeb/Pages/Templates/Packages.hs index b170f273..2025150d 100644 --- a/src/web/FloraWeb/Pages/Templates/Packages.hs +++ b/src/web/FloraWeb/Pages/Templates/Packages.hs @@ -188,11 +188,10 @@ requirementListing requirements = showChangelog :: Namespace -> PackageName -> Version -> Maybe TextHtml -> FloraHTML showChangelog namespace packageName version mChangelog = div_ [class_ "container"] $ div_ [class_ "divider"] $ do div_ [class_ "page-title"] $ - h1_ [class_ ""] $ - do - span_ [class_ "headline"] $ toHtml ("Changelog of " <> display namespace <> "/" <> display packageName) - toHtmlRaw @Text " " - span_ [class_ "version"] $ toHtml $ display version + h1_ [class_ ""] $ do + span_ [class_ "headline"] $ toHtml ("Changelog of " <> display namespace <> "/" <> display packageName) + toHtmlRaw @Text " " + span_ [class_ "version"] $ toHtml $ display version section_ [class_ "release-changelog"] $ do case mChangelog of Nothing -> toHtml @Text "This release does not have a Changelog" diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Categories.hs b/src/web/FloraWeb/Pages/Templates/Pages/Categories.hs deleted file mode 100644 index aec412b3..00000000 --- a/src/web/FloraWeb/Pages/Templates/Pages/Categories.hs +++ /dev/null @@ -1,8 +0,0 @@ -module FloraWeb.Pages.Templates.Pages.Categories - ( index - , showCategory - ) -where - -import FloraWeb.Pages.Templates.Pages.Categories.Index (index) -import FloraWeb.Pages.Templates.Pages.Categories.Show (showCategory) diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Sessions.hs b/src/web/FloraWeb/Pages/Templates/Pages/Sessions.hs deleted file mode 100644 index 6f2cfeeb..00000000 --- a/src/web/FloraWeb/Pages/Templates/Pages/Sessions.hs +++ /dev/null @@ -1,38 +0,0 @@ -module FloraWeb.Pages.Templates.Pages.Sessions where - -import FloraWeb.Pages.Templates.Types -import Lucid - -newSession :: FloraHTML -newSession = do - let formClasses = "login-form" - form_ [action_ "/sessions/new", method_ "POST", class_ formClasses] $ do - h2_ [class_ ""] "Sign in" - div_ $ do - label_ [for_ "email", class_ "sr-only"] "Email address" - input_ - [ id_ "email" - , name_ "email" - , type_ "email" - , autocomplete_ "email" - , required_ "" - , placeholder_ "Email address" - , class_ "form-input" - ] - div_ $ do - label_ [for_ "password", class_ "sr-only"] "Email address" - input_ - [ id_ "password" - , name_ "password" - , type_ "password" - , autocomplete_ "current-password" - , required_ "" - , placeholder_ "Password" - , class_ "form-input" - ] - -- div_ $ do - -- label_ [for_ "remember", class_ "text-xl mr-3"] "Remember me" - -- input_ [id_ "remember", name_ "remember", type_ "checkbox", class_ ""] - - div_ $ - button_ [type_ "submit", class_ "btn bg-brand-purple text-white w-full my-2"] "Sign in" diff --git a/src/web/FloraWeb/Pages/Templates/Screens/Categories.hs b/src/web/FloraWeb/Pages/Templates/Screens/Categories.hs new file mode 100644 index 00000000..ace512ae --- /dev/null +++ b/src/web/FloraWeb/Pages/Templates/Screens/Categories.hs @@ -0,0 +1,8 @@ +module FloraWeb.Pages.Templates.Screens.Categories + ( index + , showCategory + ) +where + +import FloraWeb.Pages.Templates.Screens.Categories.Index (index) +import FloraWeb.Pages.Templates.Screens.Categories.Show (showCategory) diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Categories/Index.hs b/src/web/FloraWeb/Pages/Templates/Screens/Categories/Index.hs similarity index 89% rename from src/web/FloraWeb/Pages/Templates/Pages/Categories/Index.hs rename to src/web/FloraWeb/Pages/Templates/Screens/Categories/Index.hs index 3d6df3af..e7bde21c 100644 --- a/src/web/FloraWeb/Pages/Templates/Pages/Categories/Index.hs +++ b/src/web/FloraWeb/Pages/Templates/Screens/Categories/Index.hs @@ -1,4 +1,4 @@ -module FloraWeb.Pages.Templates.Pages.Categories.Index where +module FloraWeb.Pages.Templates.Screens.Categories.Index where import Data.Vector (Vector) import Data.Vector qualified as V diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Categories/Show.hs b/src/web/FloraWeb/Pages/Templates/Screens/Categories/Show.hs similarity index 90% rename from src/web/FloraWeb/Pages/Templates/Pages/Categories/Show.hs rename to src/web/FloraWeb/Pages/Templates/Screens/Categories/Show.hs index 4269f9d2..c04cdbdd 100644 --- a/src/web/FloraWeb/Pages/Templates/Pages/Categories/Show.hs +++ b/src/web/FloraWeb/Pages/Templates/Screens/Categories/Show.hs @@ -1,4 +1,4 @@ -module FloraWeb.Pages.Templates.Pages.Categories.Show where +module FloraWeb.Pages.Templates.Screens.Categories.Show where import Data.Vector (Vector) import Data.Vector qualified as V diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Home.hs b/src/web/FloraWeb/Pages/Templates/Screens/Home.hs similarity index 97% rename from src/web/FloraWeb/Pages/Templates/Pages/Home.hs rename to src/web/FloraWeb/Pages/Templates/Screens/Home.hs index 358f58f4..acbe2470 100644 --- a/src/web/FloraWeb/Pages/Templates/Pages/Home.hs +++ b/src/web/FloraWeb/Pages/Templates/Screens/Home.hs @@ -1,6 +1,6 @@ {-# LANGUAGE QuasiQuotes #-} -module FloraWeb.Pages.Templates.Pages.Home where +module FloraWeb.Pages.Templates.Screens.Home where import CMarkGFM import Control.Monad.Reader diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs b/src/web/FloraWeb/Pages/Templates/Screens/Packages.hs similarity index 98% rename from src/web/FloraWeb/Pages/Templates/Pages/Packages.hs rename to src/web/FloraWeb/Pages/Templates/Screens/Packages.hs index 8fd93a65..cc1e85ad 100644 --- a/src/web/FloraWeb/Pages/Templates/Pages/Packages.hs +++ b/src/web/FloraWeb/Pages/Templates/Screens/Packages.hs @@ -1,4 +1,4 @@ -module FloraWeb.Pages.Templates.Pages.Packages where +module FloraWeb.Pages.Templates.Screens.Packages where import Data.Function ((&)) import Data.Maybe (fromMaybe) diff --git a/src/web/FloraWeb/Pages/Templates/Pages/Search.hs b/src/web/FloraWeb/Pages/Templates/Screens/Search.hs similarity index 96% rename from src/web/FloraWeb/Pages/Templates/Pages/Search.hs rename to src/web/FloraWeb/Pages/Templates/Screens/Search.hs index 13f998b6..438d03b5 100644 --- a/src/web/FloraWeb/Pages/Templates/Pages/Search.hs +++ b/src/web/FloraWeb/Pages/Templates/Screens/Search.hs @@ -1,4 +1,4 @@ -module FloraWeb.Pages.Templates.Pages.Search where +module FloraWeb.Pages.Templates.Screens.Search where import Control.Monad (when) import Data.Positive diff --git a/src/web/FloraWeb/Pages/Templates/Screens/Sessions.hs b/src/web/FloraWeb/Pages/Templates/Screens/Sessions.hs new file mode 100644 index 00000000..29f7cb68 --- /dev/null +++ b/src/web/FloraWeb/Pages/Templates/Screens/Sessions.hs @@ -0,0 +1,48 @@ +module FloraWeb.Pages.Templates.Screens.Sessions where + +import FloraWeb.Pages.Templates.Types +import Lucid + +newSession :: FloraHTML +newSession = do + let formClasses = "login-form" + form_ [action_ "/sessions/new", method_ "POST", class_ formClasses] $ do + h2_ [class_ ""] "Sign in" + label_ [for_ "email", class_ "sr-only"] "Email address" + input_ + [ id_ "email" + , name_ "email" + , type_ "email" + , autocomplete_ "email" + , required_ "" + , placeholder_ "Email address" + , class_ "form-input" + ] + label_ [for_ "password", class_ "sr-only"] "Email address" + input_ + [ id_ "password" + , name_ "password" + , type_ "password" + , autocomplete_ "current-password" + , required_ "" + , placeholder_ "Password" + , class_ "form-input" + ] + label_ [for_ "use_totp"] "Use two-factor authentication" + input_ + [ id_ "use_totp" + , name_ "use_totp" + , type_ "checkbox" + ] + div_ [class_ "totp-zone"] $ do + label_ [for_ "totp"] "Two-factor code" + input_ + [ id_ "totp" + , name_ "totp" + , type_ "text" + , pattern_ "0-9]+" + , autocomplete_ "off" + , class_ "form-input" + ] + div_ $ + button_ [type_ "submit", class_ "btn bg-brand-purple text-white w-full my-2"] "Sign in" diff --git a/src/web/FloraWeb/Pages/Templates/Screens/Settings.hs b/src/web/FloraWeb/Pages/Templates/Screens/Settings.hs new file mode 100644 index 00000000..6a935cf8 --- /dev/null +++ b/src/web/FloraWeb/Pages/Templates/Screens/Settings.hs @@ -0,0 +1,78 @@ +module FloraWeb.Pages.Templates.Screens.Settings where + +import Lucid + +import Data.Text (Text) +import Flora.Model.User +import FloraWeb.Components.Button (button) +import FloraWeb.Pages.Templates + +-- import FloraWeb.Components.Button + +dashboard :: User -> FloraHTML +dashboard user = main_ $ + div_ [class_ "container"] $ do + div_ [class_ "divider"] $ do + div_ [class_ "page-title"] $ do + h1_ "Account settings" + section_ [class_ "settings_menu"] $ do + ul_ [] $ do + li_ $ a_ [href_ "/settings/profile"] "Profile" + li_ $ a_ [href_ "/settings/security"] "Security" + +profileSettings :: User -> FloraHTML +profileSettings user = do + div_ [class_ "container"] $ do + div_ [class_ "divider"] $ do + div_ [class_ "page-title"] $ do + h1_ "Profile settings" + section_ [] $ do + form_ [] $ do + h6_ [] "User information" + div_ $ do + label_ [for_ "email"] "Email address" + input_ [type_ "text", name_ "email", id_ "email", required_ "", class_ "form-input"] + div_ $ + button_ [type_ "submit", class_ ""] "Update profile" + hr_ [class_ "settings_separator"] + +securitySettings :: FloraHTML +securitySettings = do + div_ [class_ "container"] $ do + div_ [class_ "divider"] $ do + div_ [class_ "page-title"] $ do + h1_ "Security settings" + section_ [] $ do + ul_ [] $ do + li_ [] $ + a_ [href_ "/settings/security/two-factor"] "Two-factor authentication" + +twoFactorSettings :: Text -> Text -> FloraHTML +twoFactorSettings qrCode base32Key = do + div_ [class_ "container"] $ do + div_ [class_ "divider"] $ do + div_ [class_ "page-title"] $ do + h1_ "Two-Factor Authentication" + h2_ "Scan the QR Code" + img_ [src_ ("data:image/png;base64," <> qrCode), height_ "300", width_ "300"] + toHtml base32Key + form_ [action_ "/settings/security/two-factor/setup", method_ "POST"] $ do + label_ [for_ "code"] "Code from the authenticator app" + input_ + [ id_ "code" + , name_ "code" + , type_ "text" + , required_ "" + , placeholder_ "XXXXXX" + , class_ "form-input" + ] + button "Save" + +twoFactorSettingsRemove :: FloraHTML +twoFactorSettingsRemove = do + div_ [class_ "container"] $ do + div_ [class_ "divider"] $ do + div_ [class_ "page-title"] $ do + h1_ "Two-Factor Authentication" + form_ [action_ "/settings/security/two-factor/delete", method_ "POST"] $ + button "Delete authenticator application" diff --git a/src/web/FloraWeb/Pages/Templates/Types.hs b/src/web/FloraWeb/Pages/Templates/Types.hs index 9168df6f..7a5dbbde 100644 --- a/src/web/FloraWeb/Pages/Templates/Types.hs +++ b/src/web/FloraWeb/Pages/Templates/Types.hs @@ -20,6 +20,7 @@ import GHC.Generics import Lucid import Optics.Core +import Data.Text.Display import Effectful import Effectful.Reader.Static (Reader, ask) import Flora.Environment @@ -32,13 +33,13 @@ import FloraWeb.Types type FloraHTML = HtmlT (ReaderT TemplateEnv Identity) () newtype FlashInfo = FlashInfo {getFlashInfo :: Text} - deriving (Show) via Text + deriving (Show, Display) via Text mkInfo :: Text -> FlashInfo mkInfo = FlashInfo newtype FlashError = FlashError {getFlashInfo :: Text} - deriving (Show) via Text + deriving (Show, Display) via Text mkError :: Text -> FlashError mkError = FlashError @@ -130,6 +131,6 @@ fromSession session defaults = do let TemplateDefaults{..} = defaults & (#mUser .~ muser) - & (#environment .~ (floraEnv.environment)) + & (#environment .~ floraEnv.environment) & (#features .~ featuresEnv) pure TemplateEnv{..} diff --git a/src/web/FloraWeb/Server.hs b/src/web/FloraWeb/Server.hs index 8241ad5b..071e0080 100644 --- a/src/web/FloraWeb/Server.hs +++ b/src/web/FloraWeb/Server.hs @@ -2,7 +2,6 @@ module FloraWeb.Server where import Colourista.IO (blueMessage) import Control.Exception (bracket) - import Control.Exception.Safe qualified as Safe import Control.Monad (void, when) import Data.Aeson qualified as Aeson @@ -40,6 +39,7 @@ import Optics.Core import Prometheus qualified import Prometheus.Metric.GHC (ghcMetrics) import Prometheus.Metric.Proc (procMetrics) +import Sel import Servant ( Application , Context (..) @@ -68,7 +68,7 @@ import FloraJobs.Runner (runner) import FloraJobs.Types (JobsRunnerEnv (..), makeConfig, makeUIConfig) import FloraWeb.API.Routes qualified as API import FloraWeb.API.Server qualified as API -import FloraWeb.Common.Auth (FloraAuthContext, authHandler, requestID, runVisitorSession) +import FloraWeb.Common.Auth (OptionalAuthContext, StrictAuthContext, optionalAuthHandler, requestID, runVisitorSession, strictAuthHandler) import FloraWeb.Common.Metrics import FloraWeb.Common.OpenSearch import FloraWeb.Common.Tracing @@ -83,29 +83,30 @@ import FloraWeb.Types runFlora :: IO () runFlora = - bracket - (getFloraEnv & runFailIO & runEff) - (runEff . shutdownFlora) - ( \env -> - runEff . runTime . runConcurrent $ do - let baseURL = "http://localhost:" <> display (env.httpPort) - liftIO $ blueMessage $ "🌺 Starting Flora server on " <> baseURL - liftIO $ when (isJust $ env.logging.sentryDSN) (blueMessage "📋 Connected to Sentry endpoint") - liftIO $ when env.logging.prometheusEnabled $ do - blueMessage $ "📋 Service Prometheus metrics on " <> baseURL <> "/metrics" - void $ Prometheus.register ghcMetrics - void $ Prometheus.register procMetrics - let withLogger = Logging.makeLogger (env.logging.logger) - withLogger - ( \appLogger -> - runServer appLogger env - ) - ) + secureMain $ + bracket + (getFloraEnv & runFailIO & runEff) + (runEff . shutdownFlora) + ( \env -> + runEff . runTime . runConcurrent $ do + let baseURL = "http://localhost:" <> display env.httpPort + liftIO $ blueMessage $ "🌺 Starting Flora server on " <> baseURL + liftIO $ when (isJust env.logging.sentryDSN) (blueMessage "📋 Connected to Sentry endpoint") + liftIO $ when env.logging.prometheusEnabled $ do + blueMessage $ "📋 Service Prometheus metrics on " <> baseURL <> "/metrics" + void $ Prometheus.register ghcMetrics + void $ Prometheus.register procMetrics + let withLogger = Logging.makeLogger env.logging.logger + withLogger + ( \appLogger -> + runServer appLogger env + ) + ) shutdownFlora :: FloraEnv -> Eff '[IOE] () shutdownFlora env = liftIO $ - Pool.destroyAllResources (env.pool) + Pool.destroyAllResources env.pool logException :: DeploymentEnv @@ -122,33 +123,36 @@ runServer :: (Concurrent :> es, IOE :> es) => Logger -> FloraEnv -> Eff es () runServer appLogger floraEnv = do httpManager <- liftIO $ HTTP.newManager tlsManagerSettings let runnerEnv = JobsRunnerEnv httpManager - let oddjobsUiCfg = makeUIConfig (floraEnv.config) appLogger (floraEnv.jobsPool) + let oddjobsUiCfg = makeUIConfig floraEnv.config appLogger floraEnv.jobsPool oddJobsCfg = makeConfig runnerEnv floraEnv appLogger - (floraEnv.jobsPool) + floraEnv.jobsPool runner - void $ forkIO $ unsafeEff_ $ Safe.withException (startJobRunner oddJobsCfg) (logException (floraEnv.environment) appLogger) - loggingMiddleware <- Logging.runLog (floraEnv.environment) appLogger WaiLog.mkLogMiddleware + void $ + forkIO $ + unsafeEff_ $ + Safe.withException (startJobRunner oddJobsCfg) (logException floraEnv.environment appLogger) + loggingMiddleware <- Logging.runLog floraEnv.environment appLogger WaiLog.mkLogMiddleware oddJobsEnv <- OddJobs.mkEnv oddjobsUiCfg ("/admin/odd-jobs/" <>) let webEnv = WebEnv floraEnv webEnvStore <- liftIO $ newWebEnvStore webEnv let server = mkServer appLogger webEnvStore floraEnv oddjobsUiCfg oddJobsEnv let warpSettings = - setPort (fromIntegral $ floraEnv.httpPort) $ + setPort (fromIntegral floraEnv.httpPort) $ setOnException ( onException appLogger - (floraEnv.environment) - (floraEnv.logging) + floraEnv.environment + floraEnv.logging ) defaultSettings liftIO $ runSettings warpSettings - $ prometheusMiddleware (floraEnv.environment) (floraEnv.logging) + $ prometheusMiddleware floraEnv.environment floraEnv.logging . heartbeatMiddleware . loggingMiddleware . const @@ -161,10 +165,10 @@ mkServer -> OddJobs.UIConfig -> OddJobs.Env -> Application -mkServer logger webEnvStore floraEnv cfg jobsRunnerEnv = do +mkServer logger webEnvStore floraEnv cfg jobsRunnerEnv = genericServeTWithContext - (naturalTransform (floraEnv.environment) (floraEnv.features) logger webEnvStore) - (floraServer (floraEnv.pool) cfg jobsRunnerEnv) + (naturalTransform floraEnv.environment floraEnv.features logger webEnvStore) + (floraServer floraEnv.pool cfg jobsRunnerEnv) (genAuthServerContext logger floraEnv) -- What the fuck is happening here: @@ -174,7 +178,7 @@ mkServer logger webEnvStore floraEnv cfg jobsRunnerEnv = do -- [IsVisitor, DB, Time, Reader (Headers '[Header "Set-Cookie" SetCookie] Session), Log, Error ServerError, IOE] -- api has effects: -- [DB, Time, Reader (), Log, Error ServerError, IOE] --- An the intermediate effect list of effects: +-- And the intermediate effect list of effects: -- [Reader WebEnvStore, Log, Error ServerError, IOE] -- -- What must happen is that the list of effects of 'pages' and 'api' must correspond to the intermediate 'Flora' @@ -193,7 +197,7 @@ floraServer pool cfg jobsRunnerEnv = , pages = \sessionWithCookies -> hoistServerWithContext (Proxy @Pages.Routes) - (Proxy @'[FloraAuthContext]) + (Proxy @'[OptionalAuthContext]) ( \floraPage -> floraPage & runVisitorSession @@ -229,8 +233,12 @@ naturalTransform deploymentEnv features logger webEnvStore app = & runLog deploymentEnv logger & effToHandler -genAuthServerContext :: Logger -> FloraEnv -> Context '[FloraAuthContext, ErrorFormatters] -genAuthServerContext logger floraEnv = authHandler logger floraEnv :. errorFormatters floraEnv.assets :. EmptyContext +genAuthServerContext :: Logger -> FloraEnv -> Context '[OptionalAuthContext, StrictAuthContext, ErrorFormatters] +genAuthServerContext logger floraEnv = + optionalAuthHandler logger floraEnv + :. strictAuthHandler logger floraEnv + :. errorFormatters floraEnv.assets + :. EmptyContext errorFormatters :: Assets -> ErrorFormatters errorFormatters assets = @@ -238,7 +246,10 @@ errorFormatters assets = notFoundPage :: Assets -> NotFoundErrorFormatter notFoundPage assets _req = - let result = runPureEff $ runErrorNoCallStack $ renderError (defaultsToEnv assets defaultTemplateEnv) notFound404 + let result = + runPureEff $ + runErrorNoCallStack $ + renderError (defaultsToEnv assets defaultTemplateEnv) notFound404 in case result of Left err -> err Right _ -> err404 @@ -246,6 +257,6 @@ notFoundPage assets _req = openApiHandler :: OpenApi openApiHandler = toOpenApi (Proxy @API.Routes) - & (#info % #title .~ "Flora API") - & (#info % #version .~ "v0") - & (#info % #description ?~ "Flora API Documentation") + & #info % #title .~ "Flora API" + & #info % #version .~ "v0" + & #info % #description ?~ "Flora API Documentation" diff --git a/test/Flora/TestUtils.hs b/test/Flora/TestUtils.hs index 96f802fb..8bcf60f8 100644 --- a/test/Flora/TestUtils.hs +++ b/test/Flora/TestUtils.hs @@ -290,6 +290,8 @@ genUser = do userFlags <- genUserFlags createdAt <- genUTCTime updatedAt <- genUTCTime + let totpKey = Nothing + let totpEnabled = False pure User{..} data RandomUserTemplate m = RandomUserTemplate @@ -337,4 +339,6 @@ randomUser userFlags <- generateUserFlags createdAt <- generateCreatedAt updatedAt <- generateUpdatedAt + let totpKey = Nothing + let totpEnabled = False pure User{..} From 4c7ad912a3ebd59abe0aa26c262bd8d4d594ce72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Tue, 5 Dec 2023 20:23:24 +0100 Subject: [PATCH 34/40] [NO-ISSUE] Style the sign-in button --- assets/css/styles.css | 32 +++++++++++++++++++ .../Pages/Templates/Screens/Sessions.hs | 6 ++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/assets/css/styles.css b/assets/css/styles.css index ef311f64..631dfa54 100644 --- a/assets/css/styles.css +++ b/assets/css/styles.css @@ -57,6 +57,10 @@ width: 100%; } + .password { + margin-bottom: 1rem; + } + .totp-zone { display: none; } @@ -64,6 +68,34 @@ input[type="checkbox"]:checked + div.totp-zone { display: block; } + + .login-button { + display: flex; + flex-wrap: wrap; + justify-content: center; + margin-top: 1rem; + } + + div.login-button button { + background-color: var(--main-page-button-background); + border-radius: 50rem; + border-width: 1px; + color: var(--text-color); + font-weight: bolder; + padding-bottom: 1rem; + padding-left: 2rem; + padding-right: 2rem; + padding-top: 1rem; + } + + div.login-button button:hover { + border-color: var(--main-page-button-focus-border-color); + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 200ms; + + /* offset-x | offset-y | blur-radius | spread-radius | color */ + box-shadow: 0 0 4px 2px var(--main-page-button-focus-border-color); + } } .version-list-item { diff --git a/src/web/FloraWeb/Pages/Templates/Screens/Sessions.hs b/src/web/FloraWeb/Pages/Templates/Screens/Sessions.hs index 29f7cb68..3c9217a5 100644 --- a/src/web/FloraWeb/Pages/Templates/Screens/Sessions.hs +++ b/src/web/FloraWeb/Pages/Templates/Screens/Sessions.hs @@ -26,7 +26,7 @@ newSession = do , autocomplete_ "current-password" , required_ "" , placeholder_ "Password" - , class_ "form-input" + , class_ "form-input password" ] label_ [for_ "use_totp"] "Use two-factor authentication" input_ @@ -44,5 +44,5 @@ newSession = do , autocomplete_ "off" , class_ "form-input" ] - div_ $ - button_ [type_ "submit", class_ "btn bg-brand-purple text-white w-full my-2"] "Sign in" + div_ [class_ "login-button"] $ + button_ [type_ "submit"] "Sign in" From d79142ab80af896a50761fa3f7924cd53c9d55db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:05:36 +0100 Subject: [PATCH 35/40] Bump vite from 4.4.9 to 4.4.12 in /design (#485) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- design/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/design/package-lock.json b/design/package-lock.json index 6f30c5fd..300fae26 100644 --- a/design/package-lock.json +++ b/design/package-lock.json @@ -12095,9 +12095,9 @@ } }, "node_modules/vite": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", - "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.12.tgz", + "integrity": "sha512-KtPlUbWfxzGVul8Nut8Gw2Qe8sBzWY+8QVc5SL8iRFnpnrcoCaNlzO40c1R6hPmcdTwIPEDkq0Y9+27a5tVbdQ==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -21106,9 +21106,9 @@ "dev": true }, "vite": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.9.tgz", - "integrity": "sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==", + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.4.12.tgz", + "integrity": "sha512-KtPlUbWfxzGVul8Nut8Gw2Qe8sBzWY+8QVc5SL8iRFnpnrcoCaNlzO40c1R6hPmcdTwIPEDkq0Y9+27a5tVbdQ==", "dev": true, "requires": { "esbuild": "^0.18.10", From 70523e216350db3ae5b886774822fa1bcfc56c7b Mon Sep 17 00:00:00 2001 From: Matt Roberts Date: Sun, 10 Dec 2023 15:00:02 +0000 Subject: [PATCH 36/40] [FLORA-448] Add description field in package index (#486) --- CHANGELOG.md | 3 ++- Makefile | 6 ++++-- app/cli/Main.hs | 11 +++++----- flora.cabal | 2 +- ...10311_add_description_to_package_index.sql | 2 ++ src/core/Flora/Model/PackageIndex/Types.hs | 5 +++-- src/core/Flora/Model/PackageIndex/Update.hs | 8 ++++---- .../FloraWeb/Components/PackageListHeader.hs | 2 +- src/web/FloraWeb/Pages/Server/Packages.hs | 20 +++++++++++++++++-- .../Pages/Templates/Screens/Search.hs | 6 +++--- test/Flora/ImportSpec.hs | 5 ++++- test/Main.hs | 2 +- 12 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 migrations/20231210110311_add_description_to_package_index.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 36ee64a6..ae8fcdff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,14 @@ ## 1.0.14 -- XXXX-XX-XX * Colourise in red deprecation markers on the package page ([#438](/~https://github.com/flora-pm/flora-server/pull/439)) -* Added more matches to the the natural language processing catergory ([#440](/~https://github.com/flora-pm/flora-server/pull/440)) +* Added more matches to the natural language processing category ([#440](/~https://github.com/flora-pm/flora-server/pull/440)) * Allow package imports from multiple repositories ([#444](/~https://github.com/flora-pm/flora-server/pull/444)) * Add a page on namespaces in the documentation ([#451](/~https://github.com/flora-pm/flora-server/pull/451)) * Add initial support for hosting package tarballs ([#452](/~https://github.com/flora-pm/flora-server/pull/452)) * Show depended on components in dependencies page ([#464](/~https://github.com/flora-pm/flora-server/pull/464)) * Add search bar for reverse dependencies ([#476](/~https://github.com/flora-pm/flora-server/pull/476)) * Support non Hackage repo URLs ([#479](/~https://github.com/flora-pm/flora-server/pull/479)) +* Add description field in package index ([#486](/~https://github.com/flora-pm/flora-server/pull/486)) ## 1.0.13 -- 2023-09-17 * Exclude deprecated releases from latest versions and search ([#373](/~https://github.com/flora-pm/flora-server/pull/373)) diff --git a/Makefile b/Makefile index 97e01e9b..56874d0d 100644 --- a/Makefile +++ b/Makefile @@ -45,8 +45,10 @@ db-reset: db-drop db-setup db-provision ## Reset the dev database db-provision: ## Create categories and repositories @cabal run -- flora-cli create-user --username "hackage-user" --email "tech@flora.pm" --password "foobar2000" @cabal run -- flora-cli provision categories - @cabal run -- flora-cli provision-repository --name "hackage" --url https://hackage.haskell.org - @cabal run -- flora-cli provision-repository --name "cardano" --url https://input-output-hk.github.io/cardano-haskell-packages + @cabal run -- flora-cli provision-repository --name "hackage" --url https://hackage.haskell.org \ + --description "Central package repository" + @cabal run -- flora-cli provision-repository --name "cardano" --url https://input-output-hk.github.io/cardano-haskell-packages \ + --description "Packages of the Cardano project" db-provision-test-packages: ## Load development data in the database @cabal run -- flora-cli provision test-packages diff --git a/app/cli/Main.hs b/app/cli/Main.hs index 29d56685..2002fecb 100644 --- a/app/cli/Main.hs +++ b/app/cli/Main.hs @@ -46,7 +46,7 @@ data Command | GenDesignSystemComponents | ImportPackages FilePath Text | ImportIndex FilePath Text - | ProvisionRepository Text Text + | ProvisionRepository Text Text Text | ImportPackageTarball PackageName Version FilePath deriving stock (Show, Eq) @@ -135,6 +135,7 @@ parseProvisionRepository = ProvisionRepository <$> option str (long "name" <> metavar "" <> help "Name of the repository") <*> option str (long "url" <> metavar "" <> help "Link to the package repository") + <*> option str (long "description" <> metavar "" <> help "Description of the package repository" <> value "" <> showDefault) parseImportPackageTarball :: Parser Command parseImportPackageTarball = @@ -174,12 +175,12 @@ runOptions (Options (CreateUser opts)) = do runOptions (Options GenDesignSystemComponents) = generateComponents runOptions (Options (ImportPackages path repository)) = importFolderOfCabalFiles path repository runOptions (Options (ImportIndex path repository)) = importIndex path repository -runOptions (Options (ProvisionRepository name url)) = provisionRepository name url +runOptions (Options (ProvisionRepository name url description)) = provisionRepository name url description runOptions (Options (ImportPackageTarball pname version path)) = importPackageTarball pname version path -provisionRepository :: (DB :> es, IOE :> es) => Text -> Text -> Eff es () -provisionRepository name url = do - Update.createPackageIndex name url Nothing +provisionRepository :: (DB :> es, IOE :> es) => Text -> Text -> Text -> Eff es () +provisionRepository name url description = do + Update.createPackageIndex name url description Nothing importFolderOfCabalFiles :: (Reader PoolConfig :> es, DB :> es, IOE :> es) => FilePath -> Text -> Eff es () importFolderOfCabalFiles path repository = Log.withStdOutLogger $ \appLogger -> do diff --git a/flora.cabal b/flora.cabal index dd47b17b..08fedfea 100644 --- a/flora.cabal +++ b/flora.cabal @@ -27,13 +27,13 @@ flag prod common common-extensions default-extensions: + NoStarIsType DataKinds DeriveAnyClass DerivingStrategies DerivingVia DuplicateRecordFields LambdaCase - NoStarIsType OverloadedLabels OverloadedRecordDot OverloadedStrings diff --git a/migrations/20231210110311_add_description_to_package_index.sql b/migrations/20231210110311_add_description_to_package_index.sql new file mode 100644 index 00000000..e33108bb --- /dev/null +++ b/migrations/20231210110311_add_description_to_package_index.sql @@ -0,0 +1,2 @@ +alter table package_indexes + add column description text not null; diff --git a/src/core/Flora/Model/PackageIndex/Types.hs b/src/core/Flora/Model/PackageIndex/Types.hs index 2053abc4..c7608717 100644 --- a/src/core/Flora/Model/PackageIndex/Types.hs +++ b/src/core/Flora/Model/PackageIndex/Types.hs @@ -27,6 +27,7 @@ data PackageIndex = PackageIndex , repository :: Text , timestamp :: Maybe UTCTime , url :: Text + , description :: Text } deriving stock (Eq, Show, Generic) deriving anyclass (FromRow, ToRow, NFData) @@ -34,8 +35,8 @@ data PackageIndex = PackageIndex (Entity) via (GenericEntity '[TableName "package_indexes"] PackageIndex) -mkPackageIndex :: IOE :> es => Text -> Text -> Maybe UTCTime -> Eff es PackageIndex -mkPackageIndex repository url timestamp = do +mkPackageIndex :: IOE :> es => Text -> Text -> Text -> Maybe UTCTime -> Eff es PackageIndex +mkPackageIndex repository url description timestamp = do packageIndexId <- PackageIndexId <$> liftIO UUID.nextRandom pure $ PackageIndex{..} diff --git a/src/core/Flora/Model/PackageIndex/Update.hs b/src/core/Flora/Model/PackageIndex/Update.hs index bd188c7e..5b8ef702 100644 --- a/src/core/Flora/Model/PackageIndex/Update.hs +++ b/src/core/Flora/Model/PackageIndex/Update.hs @@ -16,7 +16,7 @@ import Effectful import Effectful.PostgreSQL.Transact.Effect (DB, dbtToEff) import Flora.Model.PackageIndex.Types - ( PackageIndex + ( PackageIndex (..) , mkPackageIndex ) @@ -29,7 +29,7 @@ updatePackageIndexByName repositoryName newTimestamp = do ([field| repository |], repositoryName) (Only newTimestamp) -createPackageIndex :: (IOE :> es, DB :> es) => Text -> Text -> Maybe UTCTime -> Eff es () -createPackageIndex repositoryName url timestamp = do - packageIndex <- mkPackageIndex repositoryName url timestamp +createPackageIndex :: (IOE :> es, DB :> es) => Text -> Text -> Text -> Maybe UTCTime -> Eff es () +createPackageIndex repositoryName url description timestamp = do + packageIndex <- mkPackageIndex repositoryName url description timestamp void $ dbtToEff $ insert @PackageIndex packageIndex diff --git a/src/web/FloraWeb/Components/PackageListHeader.hs b/src/web/FloraWeb/Components/PackageListHeader.hs index e5b097f3..1aa59ec7 100644 --- a/src/web/FloraWeb/Components/PackageListHeader.hs +++ b/src/web/FloraWeb/Components/PackageListHeader.hs @@ -18,6 +18,6 @@ presentationHeader title subtitle numberOfPackages = do div_ [class_ "page-title"] $ do h1_ [class_ ""] $ do span_ [class_ "headline"] $ toHtml title - p_ [class_ "package-count"] $ toHtml $ display numberOfPackages <> " results" div_ [class_ "synopsis lg:text-xl text-center"] $ p_ [class_ ""] (toHtml subtitle) + p_ [class_ "package-count"] $ toHtml $ display numberOfPackages <> " results" diff --git a/src/web/FloraWeb/Pages/Server/Packages.hs b/src/web/FloraWeb/Pages/Server/Packages.hs index 01094d95..c72bbd65 100644 --- a/src/web/FloraWeb/Pages/Server/Packages.hs +++ b/src/web/FloraWeb/Pages/Server/Packages.hs @@ -30,6 +30,7 @@ import Flora.Logging import Flora.Model.BlobIndex.Query qualified as Query import Flora.Model.Package import Flora.Model.Package.Query qualified as Query +import Flora.Model.PackageIndex.Query qualified as Query import Flora.Model.PackageIndex.Types (PackageIndex (..)) import Flora.Model.Release.Query qualified as Query import Flora.Model.Release.Types @@ -44,6 +45,7 @@ import FloraWeb.Pages.Templates.Packages qualified as Package import FloraWeb.Pages.Templates.Screens.Packages qualified as Packages import FloraWeb.Pages.Templates.Screens.Search qualified as Search import FloraWeb.Session +import Network.HTTP.Types (notFound404) server :: ServerT Routes FloraPage server = @@ -76,8 +78,22 @@ showNamespaceHandler namespace pageParam = do session <- getSession templateDefaults <- fromSession session defaultTemplateEnv (count', results) <- Search.listAllPackagesInNamespace namespace (fromPage pageNumber) - render templateDefaults $ - Search.showAllPackagesInNamespace namespace count' pageNumber results + if extractNamespaceText namespace == "haskell" + then + render templateDefaults $ + Search.showAllPackagesInNamespace + namespace + "Core Haskell packages" + count' + pageNumber + results + else do + mPackageIndex <- Query.getPackageIndexByName (extractNamespaceText namespace) + case mPackageIndex of + Nothing -> renderError templateDefaults notFound404 + Just packageIndex -> + render templateDefaults $ + Search.showAllPackagesInNamespace namespace packageIndex.description count' pageNumber results showPackageHandler :: Namespace -> PackageName -> FloraPage (Html ()) showPackageHandler namespace packageName = showPackageVersion namespace packageName Nothing diff --git a/src/web/FloraWeb/Pages/Templates/Screens/Search.hs b/src/web/FloraWeb/Pages/Templates/Screens/Search.hs index 438d03b5..85a0c49a 100644 --- a/src/web/FloraWeb/Pages/Templates/Screens/Search.hs +++ b/src/web/FloraWeb/Pages/Templates/Screens/Search.hs @@ -21,10 +21,10 @@ showAllPackages count currentPage packagesInfo = do div_ [class_ ""] $ packageListing Nothing packagesInfo paginationNav count currentPage ListAllPackages -showAllPackagesInNamespace :: Namespace -> Word -> Positive Word -> Vector PackageInfo -> FloraHTML -showAllPackagesInNamespace namespace count currentPage packagesInfo = do +showAllPackagesInNamespace :: Namespace -> Text -> Word -> Positive Word -> Vector PackageInfo -> FloraHTML +showAllPackagesInNamespace namespace description count currentPage packagesInfo = do div_ [class_ "container"] $ do - presentationHeader (display namespace) "" count + presentationHeader (display namespace) description count div_ [class_ ""] $ packageListing Nothing packagesInfo paginationNav count currentPage (ListAllPackagesInNamespace namespace) diff --git a/test/Flora/ImportSpec.hs b/test/Flora/ImportSpec.hs index a25bea82..68429e56 100644 --- a/test/Flora/ImportSpec.hs +++ b/test/Flora/ImportSpec.hs @@ -35,12 +35,15 @@ defaultRepo = "test-namespace" defaultRepoURL :: Text defaultRepoURL = "localhost" +defaultDescription :: Text +defaultDescription = "test-description" + testImportIndex :: Fixtures -> TestEff () testImportIndex fixture = withStdOutLogger $ \logger -> do mIndex <- Query.getPackageIndexByName defaultRepo case mIndex of - Nothing -> Update.createPackageIndex defaultRepo defaultRepoURL Nothing + Nothing -> Update.createPackageIndex defaultRepo defaultRepoURL defaultDescription Nothing Just _ -> pure () importFromIndex logger diff --git a/test/Main.hs b/test/Main.hs index 37e1e4c7..ca210cbb 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -37,7 +37,7 @@ main = do . runBlobStorePure . runFailIO $ do - Update.createPackageIndex "hackage" "" Nothing + Update.createPackageIndex "hackage" "" "" Nothing testMigrations f' <- getFixtures importAllPackages f' From bd5890e368fe0fa76fe4895ef9ae329f18949d04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sun, 10 Dec 2023 16:28:52 +0100 Subject: [PATCH 37/40] [NO-ISSUE] Enforce the 'canLogin' property --- flora.cabal | 2 +- src/core/Flora/Model/User.hs | 2 +- src/web/FloraWeb/Common/Auth.hs | 2 +- src/web/FloraWeb/Pages/Server/Sessions.hs | 28 +++++++++++++++-------- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/flora.cabal b/flora.cabal index 08fedfea..dd47b17b 100644 --- a/flora.cabal +++ b/flora.cabal @@ -27,13 +27,13 @@ flag prod common common-extensions default-extensions: - NoStarIsType DataKinds DeriveAnyClass DerivingStrategies DerivingVia DuplicateRecordFields LambdaCase + NoStarIsType OverloadedLabels OverloadedRecordDot OverloadedStrings diff --git a/src/core/Flora/Model/User.hs b/src/core/Flora/Model/User.hs index 8bf454c7..3c54d7d4 100644 --- a/src/core/Flora/Model/User.hs +++ b/src/core/Flora/Model/User.hs @@ -140,7 +140,7 @@ mkAdmin AdminCreationForm{username, email, password} = do let createdAt = timestamp let updatedAt = timestamp let displayName = "" - let userFlags = UserFlags{isAdmin = True, canLogin = False} + let userFlags = UserFlags{isAdmin = True, canLogin = True} let totpKey = Nothing let totpEnabled = False pure User{..} diff --git a/src/web/FloraWeb/Common/Auth.hs b/src/web/FloraWeb/Common/Auth.hs index 73830461..ebb896dd 100644 --- a/src/web/FloraWeb/Common/Auth.hs +++ b/src/web/FloraWeb/Common/Auth.hs @@ -81,7 +81,7 @@ handler mustBeConnected floraEnv req = do case mUserInfo of Nothing -> if mustBeConnected - then throwError $ err401{errBody = "Connect first"} + then throwError $ err401{errBody = "Log-in first"} else do nSessionId <- liftIO newPersistentSessionId pure (Nothing, nSessionId) diff --git a/src/web/FloraWeb/Pages/Server/Sessions.hs b/src/web/FloraWeb/Pages/Server/Sessions.hs index ba922cc8..44f83cdc 100644 --- a/src/web/FloraWeb/Pages/Server/Sessions.hs +++ b/src/web/FloraWeb/Pages/Server/Sessions.hs @@ -58,18 +58,26 @@ createSessionHandler LoginForm{email, password, totp} = do & (#flashError ?~ mkError "Could not authenticate") respond $ WithStatus @401 $ renderUVerb templateEnv Sessions.newSession Just user -> - if validatePassword (mkPassword password) user.password - then do - if user.totpEnabled - then guardThatUserHasProvidedTOTP totp $ \userCode -> do - checkTOTPIsValid userCode user + if user.userFlags.canLogin + then + if validatePassword (mkPassword password) user.password + then do + if user.totpEnabled + then guardThatUserHasProvidedTOTP totp $ \userCode -> do + checkTOTPIsValid userCode user + else do + sessionId <- persistSession session.sessionId user.userId + let sessionCookie = craftSessionCookie sessionId True + respond $ WithStatus @301 $ redirectWithCookie "/" sessionCookie else do - Log.logInfo_ "[+] User connected!" - sessionId <- persistSession session.sessionId user.userId - let sessionCookie = craftSessionCookie sessionId True - respond $ WithStatus @301 $ redirectWithCookie "/" sessionCookie + Log.logInfo_ "Invalid password" + templateDefaults <- fromSession session defaultTemplateEnv + let templateEnv = + templateDefaults + & (#flashError ?~ mkError "Could not authenticate") + respond $ WithStatus @401 $ renderUVerb templateEnv Sessions.newSession else do - Log.logInfo_ "[+] Couldn't authenticate user" + Log.logInfo_ "User not allowed to log-in" templateDefaults <- fromSession session defaultTemplateEnv let templateEnv = templateDefaults From 4da0f0d8a7a8c4a1b04f409f4a09907e32456973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Sun, 10 Dec 2023 16:29:17 +0100 Subject: [PATCH 38/40] [NO-ISSUE] Add a default to the description in package index migration --- migrations/20231210110311_add_description_to_package_index.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migrations/20231210110311_add_description_to_package_index.sql b/migrations/20231210110311_add_description_to_package_index.sql index e33108bb..14ee8e13 100644 --- a/migrations/20231210110311_add_description_to_package_index.sql +++ b/migrations/20231210110311_add_description_to_package_index.sql @@ -1,2 +1,2 @@ alter table package_indexes - add column description text not null; + add column description text not null default ''; From 0bf3d8838500e663c6d556f0c9b65b1407bcb136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Wed, 13 Dec 2023 15:05:46 +0100 Subject: [PATCH 39/40] [FLORA-35] Search bar modifiers (#487) --- CHANGELOG.md | 1 + docs/docs/intro.md | 1 + docs/docs/search.md | 14 ++ flora.cabal | 4 +- scripts/run-tests.sh | 5 - src/core/Flora/Model/Package/Query.hs | 63 ++++++++- src/core/Flora/Model/Package/Types.hs | 6 +- src/core/Flora/Search.hs | 141 ++++++++++++++++++- src/web/FloraWeb/Components/Navbar.hs | 8 +- src/web/FloraWeb/Pages/Server/Packages.hs | 25 +++- src/web/FloraWeb/Pages/Server/Search.hs | 19 +-- src/web/FloraWeb/Pages/Templates/Packages.hs | 11 +- src/web/FloraWeb/Pages/Templates/Types.hs | 3 + test/Flora/PackageSpec.hs | 4 +- test/Flora/SearchSpec.hs | 45 ++++++ test/Main.hs | 14 +- 16 files changed, 317 insertions(+), 47 deletions(-) create mode 100644 docs/docs/search.md create mode 100644 test/Flora/SearchSpec.hs diff --git a/CHANGELOG.md b/CHANGELOG.md index ae8fcdff..e08058ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * Add search bar for reverse dependencies ([#476](/~https://github.com/flora-pm/flora-server/pull/476)) * Support non Hackage repo URLs ([#479](/~https://github.com/flora-pm/flora-server/pull/479)) * Add description field in package index ([#486](/~https://github.com/flora-pm/flora-server/pull/486)) +* Introduce search bar modifiers ([#487](/~https://github.com/flora-pm/flora-server/pull/487)) ## 1.0.13 -- 2023-09-17 * Exclude deprecated releases from latest versions and search ([#373](/~https://github.com/flora-pm/flora-server/pull/373)) diff --git a/docs/docs/intro.md b/docs/docs/intro.md index 5bac245c..1c856170 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -6,3 +6,4 @@ slug: / Read more about: * [Namespaces](/namespaces) +* [Search features](/search-features) diff --git a/docs/docs/search.md b/docs/docs/search.md new file mode 100644 index 00000000..448cadc5 --- /dev/null +++ b/docs/docs/search.md @@ -0,0 +1,14 @@ +--- +title: Search features +slug: search-features +--- + +While searching for packages you may want to refine the search terms with modifiers. +Currently, the following modifiers are available: + +* `depends:<@namespace>/`: Shows the dependents page for a package +* `in:<@namespace> `: Searches for a package name in the specified namespace +* `in:<@namespace>`: Lists packages in a namespace + +These modifiers must be placed at the very beginning of the search query, otherwise they will +be interpreted as a search term. diff --git a/flora.cabal b/flora.cabal index dd47b17b..5c8f31ef 100644 --- a/flora.cabal +++ b/flora.cabal @@ -170,7 +170,6 @@ library , monad-time-effectful , mtl , odd-jobs - , one-time-password , openapi3 , optics-core , password @@ -465,6 +464,7 @@ test-suite flora-test , monad-time-effectful , optics-core , password + , password-types , pg-entity , pg-transact , pg-transact-effectful @@ -480,6 +480,7 @@ test-suite flora-test , text , time , transformers + , typed-process , uuid , vector , zlib @@ -491,6 +492,7 @@ test-suite flora-test Flora.ImportSpec Flora.OddJobSpec Flora.PackageSpec + Flora.SearchSpec Flora.TemplateSpec Flora.TestUtils Flora.UserSpec diff --git a/scripts/run-tests.sh b/scripts/run-tests.sh index 72b11e41..9ac307c1 100755 --- a/scripts/run-tests.sh +++ b/scripts/run-tests.sh @@ -7,11 +7,6 @@ source ./environment.test.sh export DATALOG_DIR="cbits/" -make db-drop -make db-setup - -cabal run -- flora-cli create-user --username "hackage-user" --email "tech@flora.pm" --password "foobar2000" - if [ -z "$1" ] ; then cabal test diff --git a/src/core/Flora/Model/Package/Query.hs b/src/core/Flora/Model/Package/Query.hs index 18298c0b..46f9134c 100644 --- a/src/core/Flora/Model/Package/Query.hs +++ b/src/core/Flora/Model/Package/Query.hs @@ -1,7 +1,34 @@ {-# LANGUAGE OverloadedLists #-} {-# LANGUAGE QuasiQuotes #-} -module Flora.Model.Package.Query where +module Flora.Model.Package.Query + ( countPackages + , countPackagesByName + , countPackagesInNamespace + , getAllPackageDependents + , getAllPackageDependentsWithLatestVersion + , getAllPackages + , getAllRequirements + , getComponent + , getNonDeprecatedPackages + , getNumberOfPackageDependents + , getPackageByNamespaceAndName + , getPackageCategories + , getPackageDependents + , getPackageDependentsByName + , getPackageDependentsWithLatestVersion + , getPackagesByNamespace + , getPackagesFromCategoryWithLatestVersion + , getRequirements + , listAllPackages + , listAllPackagesInNamespace + , numberOfPackageRequirementsQuery + , searchPackage + , unsafeGetComponent + , getComponentById + , searchPackageByNamespace + , getNumberOfPackageRequirements + ) where import Data.Text (Text) import Data.Text.Display (display) @@ -437,6 +464,40 @@ searchPackage (offset, limit) searchString = |] (searchString, searchString, offset, limit) +searchPackageByNamespace + :: DB :> es + => (Word, Word) + -> Namespace + -> Text + -> Eff es (Vector PackageInfo) +searchPackageByNamespace (offset, limit) namespace searchString = + dbtToEff $ + query + Select + [sql| + SELECT lv."namespace" + , lv."name" + , lv."synopsis" + , lv."version" + , lv."license" + , word_similarity(lv.name, ?) as rating + FROM latest_versions as lv + WHERE + ? <% lv."name" + AND lv."namespace" = ? + GROUP BY + lv."namespace" + , lv."name" + , lv."synopsis" + , lv."version" + , lv."license" + ORDER BY rating desc, count(lv."namespace") desc, lv.name asc + OFFSET ? + LIMIT ? + ; + |] + (searchString, searchString, namespace, offset, limit) + -- | Returns a summary of packages listAllPackages :: DB :> es diff --git a/src/core/Flora/Model/Package/Types.hs b/src/core/Flora/Model/Package/Types.hs index 3d9ecb21..86f7b95e 100644 --- a/src/core/Flora/Model/Package/Types.hs +++ b/src/core/Flora/Model/Package/Types.hs @@ -85,7 +85,7 @@ extractPackageNameText (PackageName text) = text parsePackageName :: Text -> Maybe PackageName parsePackageName txt = - if matches "[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*" txt + if matches "^[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*$" txt then Just $ PackageName txt else Nothing @@ -109,7 +109,7 @@ packageNameSchema :: Schema packageNameSchema = mempty & #description - ?~ "Name of a package\n It corresponds to the regular expression: `[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*`" + ?~ "Name of a package\n It corresponds to the regular expression: `^[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*$`" newtype Namespace = Namespace Text deriving stock (Show, Generic) @@ -148,7 +148,7 @@ instance FromHttpApiData Namespace where parseNamespace :: Text -> Maybe Namespace parseNamespace txt = - if matches "@[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*" txt + if matches "^@[[:digit:]]*[[:alpha:]][[:alnum:]]*(-[[:digit:]]*[[:alpha:]][[:alnum:]]*)*$" txt then Just $ Namespace txt else Nothing diff --git a/src/core/Flora/Search.hs b/src/core/Flora/Search.hs index cf0feaa1..ccfc637a 100644 --- a/src/core/Flora/Search.hs +++ b/src/core/Flora/Search.hs @@ -1,8 +1,14 @@ +{-# LANGUAGE ViewPatterns #-} +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} + +{-# HLINT ignore "Use <$>" #-} + module Flora.Search where import Data.Aeson import Data.List qualified as List import Data.Text (Text) +import Data.Text qualified as Text import Data.Text.Display (Display (..)) import Data.Text.Lazy.Builder qualified as Builder import Data.Vector (Vector) @@ -16,12 +22,21 @@ import Log qualified import Flora.Logging import Flora.Model.Package (Namespace (..), PackageInfo (..), PackageName (..), formatPackage) import Flora.Model.Package.Query qualified as Query +import Flora.Model.Package.Types qualified as Package +import Flora.Model.Requirement data SearchAction = ListAllPackages | ListAllPackagesInNamespace Namespace | SearchPackages Text - | DependentsOf Namespace PackageName (Maybe Text) + | DependentsOf + Namespace + -- ^ Namespace + PackageName + -- ^ Package + (Maybe Text) + -- ^ Search within the package + | SearchInNamespace Namespace PackageName deriving (Eq, Ord, Show) instance Display SearchAction where @@ -34,6 +49,22 @@ instance Display SearchAction where <> "/" <> displayBuilder packageName <> foldMap (\searchString -> " \"" <> Builder.fromText searchString <> "\"") mbSearchString + displayBuilder (SearchInNamespace namespace packageName) = + "Package " <> displayBuilder namespace <> "/" <> displayBuilder packageName + +search + :: (DB :> es, Log :> es, Time :> es) + => (Word, Word) + -> Text + -> Eff es (Word, Vector PackageInfo) +search pagination queryString = + case parseSearchQuery queryString of + Just (ListAllPackagesInNamespace namespace) -> listAllPackagesInNamespace pagination namespace + Just ListAllPackages -> listAllPackages pagination + Just (SearchInNamespace namespace (PackageName packageName)) -> searchPackageByNamespaceAndName pagination namespace packageName + Just (DependentsOf namespace packageName mSearchString) -> searchDependents pagination namespace packageName mSearchString + Just (SearchPackages _) -> searchPackageByName pagination queryString + Nothing -> searchPackageByName pagination queryString searchPackageByName :: (DB :> es, Log :> es, Time :> es) @@ -62,13 +93,68 @@ searchPackageByName (offset, limit) queryString = do count <- Query.countPackagesByName queryString pure (count, results) +searchPackageByNamespaceAndName + :: (DB :> es, Log :> es, Time :> es) + => (Word, Word) + -> Namespace + -> Text + -> Eff es (Word, Vector PackageInfo) +searchPackageByNamespaceAndName (offset, limit) namespace queryString = do + (results, duration) <- timeAction $ Query.searchPackageByNamespace (offset, limit) namespace queryString + + Log.logInfo "search-results" $ + object + [ "search_string" .= queryString + , "duration" .= duration + , "results_count" .= Vector.length results + , "results" + .= List.map + ( \PackageInfo{name, rating} -> + object + [ "package" .= formatPackage namespace name + , "score" .= rating + ] + ) + (Vector.toList results) + ] + + count <- Query.countPackagesByName queryString + pure (count, results) + +searchDependents + :: DB :> es + => (Word, Word) + -> Namespace + -> PackageName + -> Maybe Text + -> Eff es (Word, Vector PackageInfo) +searchDependents pagination namespace packageName mSearchString = do + results <- + Query.getAllPackageDependentsWithLatestVersion + namespace + packageName + pagination + mSearchString + totalDependents <- Query.getNumberOfPackageDependents namespace packageName mSearchString + pure (totalDependents, fmap dependencyInfoToPackageInfo results) + +dependencyInfoToPackageInfo :: DependencyInfo -> PackageInfo +dependencyInfoToPackageInfo dep = + PackageInfo + dep.namespace + dep.name + dep.latestSynopsis + dep.latestVersion + dep.latestLicense + Nothing + listAllPackagesInNamespace :: (DB :> es, Time :> es, Log :> es) - => Namespace - -> (Word, Word) + => (Word, Word) + -> Namespace -> Eff es (Word, Vector PackageInfo) -listAllPackagesInNamespace namespace (offset, limit) = do - (results, duration) <- timeAction $ Query.listAllPackagesInNamespace (offset, limit) namespace +listAllPackagesInNamespace pagination namespace = do + (results, duration) <- timeAction $ Query.listAllPackagesInNamespace pagination namespace Log.logInfo "packages-in-namespace" $ object @@ -98,3 +184,48 @@ listAllPackages (offset, limit) = do results <- Query.listAllPackages (offset, limit) count <- Query.countPackages pure (count, results) + +-- | Search modifiers: +-- +-- * depends:<@namespace>/ +-- * in:<@namespace>/ +-- * in:<@namespace> +parseSearchQuery :: Text -> Maybe SearchAction +parseSearchQuery = \case + (Text.stripPrefix "depends:" -> Just rest) -> + case parseNamespacedPackageSearch rest of + Just (namespace, packageName) -> + Just $ DependentsOf namespace packageName Nothing + Nothing -> Just $ SearchPackages rest + (Text.stripPrefix "in:" -> Just rest) -> + case parseNamespaceAndPackageSearch rest of + (Just namespace, Just packageName) -> + Just $ SearchInNamespace namespace packageName + (Just namespace, Nothing) -> + Just $ ListAllPackagesInNamespace namespace + _ -> Just $ SearchPackages rest + e -> Just $ SearchPackages e + +-- Determine if the string is +-- <@namespace>/ +parseNamespacedPackageSearch :: Text -> Maybe (Namespace, PackageName) +parseNamespacedPackageSearch text = + case Text.breakOn "/" text of + (_, "") -> Nothing + (Package.parseNamespace -> Just namespace, Text.stripPrefix "/" -> Just potentialPackageName) -> + case Package.parsePackageName potentialPackageName of + Just packageName -> Just (namespace, packageName) + Nothing -> Nothing + (_, _) -> Nothing + +parseNamespaceAndPackageSearch :: Text -> (Maybe Namespace, Maybe PackageName) +parseNamespaceAndPackageSearch text = + case Text.breakOn " " text of + (Package.parseNamespace -> Just namespace, "") -> + (Just namespace, Nothing) + (_, "") -> (Nothing, Nothing) + (Package.parseNamespace -> Just namespace, Text.stripPrefix " " -> Just potentialPackageName) -> + case Package.parsePackageName potentialPackageName of + Just packageName -> (Just namespace, Just packageName) + Nothing -> (Just namespace, Nothing) + (_, _) -> (Nothing, Nothing) diff --git a/src/web/FloraWeb/Components/Navbar.hs b/src/web/FloraWeb/Components/Navbar.hs index b7e8ea62..8c0c1f41 100644 --- a/src/web/FloraWeb/Components/Navbar.hs +++ b/src/web/FloraWeb/Components/Navbar.hs @@ -110,18 +110,24 @@ userMenu = do navbarSearch :: FloraHTML navbarSearch = do flag <- asks displayNavbarSearch + mContent <- asks navbarSearchContent if flag then do + let contentValue = + case mContent of + Nothing -> [] + Just content -> [value_ content] form_ [action_ "/search", method_ "GET"] $ do div_ [class_ "flex items-center py-2"] $ do label_ [for_ "search"] "" - input_ + input_ $ [ class_ "navbar-search" , id_ "search" , type_ "search" , name_ "q" , placeholder_ "Search a package" ] + ++ contentValue else pure mempty logOff :: Maybe User -> PersistentSessionId -> FloraHTML diff --git a/src/web/FloraWeb/Pages/Server/Packages.hs b/src/web/FloraWeb/Pages/Server/Packages.hs index c72bbd65..a8666590 100644 --- a/src/web/FloraWeb/Pages/Server/Packages.hs +++ b/src/web/FloraWeb/Pages/Server/Packages.hs @@ -77,13 +77,19 @@ showNamespaceHandler namespace pageParam = do let pageNumber = pageParam ?: PositiveUnsafe 1 session <- getSession templateDefaults <- fromSession session defaultTemplateEnv - (count', results) <- Search.listAllPackagesInNamespace namespace (fromPage pageNumber) + (count', results) <- Search.listAllPackagesInNamespace (fromPage pageNumber) namespace if extractNamespaceText namespace == "haskell" - then - render templateDefaults $ + then do + let description = "Core Haskell packages" + let templateEnv = + templateDefaults + { navbarSearchContent = Just $ "in:" <> display namespace <> " " + , description = description + } + render templateEnv $ Search.showAllPackagesInNamespace namespace - "Core Haskell packages" + description count' pageNumber results @@ -91,8 +97,13 @@ showNamespaceHandler namespace pageParam = do mPackageIndex <- Query.getPackageIndexByName (extractNamespaceText namespace) case mPackageIndex of Nothing -> renderError templateDefaults notFound404 - Just packageIndex -> - render templateDefaults $ + Just packageIndex -> do + let templateEnv = + templateDefaults + { navbarSearchContent = Just $ "in:" <> display namespace <> " " + , description = packageIndex.description + } + render templateEnv $ Search.showAllPackagesInNamespace namespace packageIndex.description count' pageNumber results showPackageHandler :: Namespace -> PackageName -> FloraPage (Html ()) @@ -194,6 +205,7 @@ showVersionDependentsHandler namespace packageName version (Just pageNumber) mSe templateEnv' { title = display namespace <> "/" <> display packageName , description = "Dependents of " <> display namespace <> display packageName + , navbarSearchContent = Just $ "depends:" <> display namespace <> "/" <> display packageName <> " " } results <- Query.getAllPackageDependentsWithLatestVersion @@ -211,7 +223,6 @@ showVersionDependentsHandler namespace packageName version (Just pageNumber) mSe totalDependents results pageNumber - mSearch showDependenciesHandler :: Namespace -> PackageName -> FloraPage (Html ()) showDependenciesHandler namespace packageName = do diff --git a/src/web/FloraWeb/Pages/Server/Search.hs b/src/web/FloraWeb/Pages/Server/Search.hs index 244dae95..91cc1a76 100644 --- a/src/web/FloraWeb/Pages/Server/Search.hs +++ b/src/web/FloraWeb/Pages/Server/Search.hs @@ -4,14 +4,13 @@ import Data.Positive import Data.Text (Text) import Data.Vector qualified as Vector import Lucid (Html) -import Optics.Core import Servant (ServerT) import Flora.Model.Package.Types import Flora.Search qualified as Search import FloraWeb.Common.Pagination import FloraWeb.Pages.Routes.Search (Routes, Routes' (..)) -import FloraWeb.Pages.Templates (TemplateEnv (..), defaultTemplateEnv, fromSession, render) +import FloraWeb.Pages.Templates import FloraWeb.Pages.Templates.Screens.Search qualified as Search import FloraWeb.Session @@ -23,18 +22,14 @@ server = searchHandler :: Maybe Text -> Maybe (Positive Word) -> FloraPage (Html ()) searchHandler Nothing pageParam = searchHandler (Just "") pageParam -searchHandler (Just "") pageParam = do - let pageNumber = pageParam ?: PositiveUnsafe 1 - session <- getSession - templateDefaults <- fromSession session defaultTemplateEnv - (count, results) <- Search.listAllPackages (fromPage pageNumber) - let (templateEnv :: TemplateEnv) = - templateDefaults & #displayNavbarSearch .~ False - render templateEnv $ Search.showAllPackages count pageNumber results searchHandler (Just searchString) pageParam = do let pageNumber = pageParam ?: PositiveUnsafe 1 session <- getSession - templateEnv <- fromSession session defaultTemplateEnv - (count, results) <- Search.searchPackageByName (fromPage pageNumber) searchString + templateDefaults <- fromSession session defaultTemplateEnv + let templateEnv = + templateDefaults + { navbarSearchContent = Just searchString + } + (count, results) <- Search.search (fromPage pageNumber) searchString let (matchVector, packagesInfo) = Vector.partition (\p -> p.name == PackageName searchString) results render templateEnv $ Search.showResults searchString count pageNumber matchVector packagesInfo diff --git a/src/web/FloraWeb/Pages/Templates/Packages.hs b/src/web/FloraWeb/Pages/Templates/Packages.hs index 2025150d..1a13aeb5 100644 --- a/src/web/FloraWeb/Pages/Templates/Packages.hs +++ b/src/web/FloraWeb/Pages/Templates/Packages.hs @@ -6,7 +6,7 @@ import Control.Monad.Reader (ask) import Data.Foldable (fold, forM_) import Data.List qualified as List import Data.Map.Strict qualified as Map -import Data.Maybe (fromJust, fromMaybe, isJust) +import Data.Maybe (fromJust, isJust) import Data.Positive import Data.Text (Text) import Data.Text qualified as Text @@ -37,7 +37,6 @@ import Flora.Search (SearchAction (..)) import FloraWeb.Components.Icons import FloraWeb.Components.PackageListItem (licenseIcon, packageListItem, requirementListItem) import FloraWeb.Components.PaginationNav (paginationNav) -import FloraWeb.Components.SlimSearchBar import FloraWeb.Components.Utils import FloraWeb.Links qualified as Links import FloraWeb.Pages.Templates (FloraHTML, TemplateEnv (..)) @@ -66,7 +65,7 @@ presentationHeaderForSubpage namespace packageName release target numberOfPackag span_ [class_ "headline"] $ do displayNamespace namespace chevronRightOutline - linkToPackageWithVersion namespace packageName (release.version) + linkToPackageWithVersion namespace packageName release.version chevronRightOutline toHtml (display target) p_ [class_ "synopsis"] $ @@ -101,15 +100,11 @@ showDependents -> Word -> Vector DependencyInfo -> Positive Word - -> Maybe Text -> FloraHTML -showDependents namespace packageName release count packagesInfo currentPage mSearch = +showDependents namespace packageName release count packagesInfo currentPage = div_ [class_ "container"] $ do presentationHeaderForSubpage namespace packageName release Dependents count - let placeholder = fromMaybe "Search dependents" mSearch - let value = fromMaybe "" mSearch ul_ [class_ "package-list"] $ do - slimSearchBar (SearchBarOptions{actionUrl = "", placeholder, value}) Vector.forM_ packagesInfo ( \dep -> diff --git a/src/web/FloraWeb/Pages/Templates/Types.hs b/src/web/FloraWeb/Pages/Templates/Types.hs index 7a5dbbde..b1987aa7 100644 --- a/src/web/FloraWeb/Pages/Templates/Types.hs +++ b/src/web/FloraWeb/Pages/Templates/Types.hs @@ -58,6 +58,7 @@ data TemplateEnv = TemplateEnv , activeElements :: ActiveElements , assets :: Assets , indexPage :: Bool + , navbarSearchContent :: Maybe Text } deriving stock (Show, Generic) @@ -82,6 +83,7 @@ data TemplateDefaults = TemplateDefaults , features :: FeatureEnv , activeElements :: ActiveElements , indexPage :: Bool + , navbarSearchContent :: Maybe Text } deriving stock (Show, Generic) @@ -108,6 +110,7 @@ defaultTemplateEnv = , features = FeatureEnv Nothing , activeElements = defaultActiveElements , indexPage = True + , navbarSearchContent = Nothing } -- | ⚠ DO NOT USE THIS FUNCTION IF YOU DON'T KNOW WHAT YOU'RE DOING diff --git a/test/Flora/PackageSpec.hs b/test/Flora/PackageSpec.hs index 4ef929fc..029f0606 100644 --- a/test/Flora/PackageSpec.hs +++ b/test/Flora/PackageSpec.hs @@ -228,9 +228,9 @@ testReleaseDeprecation = do assertEqual 68 (length result) binary <- fromJust <$> Query.getPackageByNamespaceAndName (Namespace "haskell") (PackageName "binary") - deprecatedBinaryVersion' <- assertJust =<< Query.getReleaseByVersion (binary.packageId) (mkVersion [0, 10, 0, 0]) + deprecatedBinaryVersion' <- assertJust =<< Query.getReleaseByVersion binary.packageId (mkVersion [0, 10, 0, 0]) Update.setReleasesDeprecationMarker (Vector.singleton (True, deprecatedBinaryVersion'.releaseId)) - deprecatedBinaryVersion <- assertJust =<< Query.getReleaseByVersion (binary.packageId) (mkVersion [0, 10, 0, 0]) + deprecatedBinaryVersion <- assertJust =<< Query.getReleaseByVersion binary.packageId (mkVersion [0, 10, 0, 0]) assertEqual deprecatedBinaryVersion.deprecated (Just True) --- diff --git a/test/Flora/SearchSpec.hs b/test/Flora/SearchSpec.hs new file mode 100644 index 00000000..9463df0d --- /dev/null +++ b/test/Flora/SearchSpec.hs @@ -0,0 +1,45 @@ +module Flora.SearchSpec where + +import Test.Tasty + +import Flora.Model.Package.Types +import Flora.Search +import Flora.TestUtils + +spec :: TestEff TestTree +spec = + testThese + "Search bar mdifiers" + [ testThis "Parsing of \"depends:<@namespace>/\" search modifier" testParsingDependsSearchModifier + , testThis "Parsing of \"in:<@namespace> \" modifier" testParsingNamespacePackageModifier + , testThis "Parsing of \"in:<@namespace>\" modifier" testParsingNamespaceModifier + , testThis "Parsing of a query containing a modifier" testParsingQueryContainingModifier + ] + +testParsingDependsSearchModifier :: TestEff () +testParsingDependsSearchModifier = do + let result = parseSearchQuery "depends:@haskell/base" + assertEqual + (Just $ DependentsOf (Namespace "@haskell") (PackageName "base") Nothing) + result + +testParsingNamespacePackageModifier :: TestEff () +testParsingNamespacePackageModifier = do + let result = parseSearchQuery "in:@haskell base" + assertEqual + (Just $ SearchInNamespace (Namespace "@haskell") (PackageName "base")) + result + +testParsingNamespaceModifier :: TestEff () +testParsingNamespaceModifier = do + let result = parseSearchQuery "in:@haskell" + assertEqual + (Just $ ListAllPackagesInNamespace (Namespace "@haskell")) + result + +testParsingQueryContainingModifier :: TestEff () +testParsingQueryContainingModifier = do + let result = parseSearchQuery "bah blah blah depends:@haskell/base" + assertEqual + (Just (SearchPackages "bah blah blah depends:@haskell/base")) + result diff --git a/test/Main.hs b/test/Main.hs index ca210cbb..c921283b 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -1,5 +1,6 @@ module Main where +import Data.Password.Types import Effectful import Effectful.Fail (runFailIO) import Effectful.Log qualified as Log @@ -9,18 +10,21 @@ import Effectful.Time import Log.Backend.StandardOutput qualified as Log import Log.Data import System.IO +import System.Process.Typed qualified as Process import Test.Tasty (defaultMain, testGroup) -import Flora.Model.BlobStore.API - import Flora.BlobSpec qualified as BlobSpec import Flora.CabalSpec qualified as CabalSpec import Flora.CategorySpec qualified as CategorySpec import Flora.Environment import Flora.ImportSpec qualified as ImportSpec +import Flora.Model.BlobStore.API import Flora.Model.PackageIndex.Update qualified as Update +import Flora.Model.User (UserCreationForm (..), hashPassword, mkUser) +import Flora.Model.User.Update qualified as Update import Flora.OddJobSpec qualified as OddJobSpec import Flora.PackageSpec qualified as PackageSpec +import Flora.SearchSpec qualified as SearchSpec import Flora.TemplateSpec qualified as TemplateSpec import Flora.TestUtils import Flora.UserSpec qualified as UserSpec @@ -28,6 +32,8 @@ import Flora.UserSpec qualified as UserSpec main :: IO () main = do hSetBuffering stdout LineBuffering + Process.runProcess "make db-drop" + Process.runProcess "make db-setup" env <- runEff getFloraTestEnv fixtures <- runEff $ Log.withStdOutLogger $ \stdOutLogger -> do runTime @@ -38,6 +44,9 @@ main = do . runFailIO $ do Update.createPackageIndex "hackage" "" "" Nothing + password <- hashPassword $ mkPassword "foobar2000" + templateUser <- mkUser $ UserCreationForm "hackage-user" "tech@flora.pm" password + Update.insertUser templateUser testMigrations f' <- getFixtures importAllPackages f' @@ -54,4 +63,5 @@ specs fixtures = , CabalSpec.spec , ImportSpec.spec fixtures , BlobSpec.spec + , SearchSpec.spec ] From 52550e593afd7002d30c9a0a9d1736017c1d0b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Wed, 13 Dec 2023 17:29:09 +0100 Subject: [PATCH 40/40] [NO-ISSUE] Flora 1.0.14 (#488) --- CHANGELOG.md | 2 +- flora.cabal | 2 +- src/core/Flora/Import/Package.hs | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e08058ca..02f0aaff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # CHANGELOG -## 1.0.14 -- XXXX-XX-XX +## 1.0.14 -- 2023-12-13 * Colourise in red deprecation markers on the package page ([#438](/~https://github.com/flora-pm/flora-server/pull/439)) * Added more matches to the natural language processing category ([#440](/~https://github.com/flora-pm/flora-server/pull/440)) * Allow package imports from multiple repositories ([#444](/~https://github.com/flora-pm/flora-server/pull/444)) diff --git a/flora.cabal b/flora.cabal index 5c8f31ef..c39e599e 100644 --- a/flora.cabal +++ b/flora.cabal @@ -1,6 +1,6 @@ cabal-version: 3.0 name: flora -version: 1.0.13 +version: 1.0.14 homepage: /~https://github.com/flora-pm/flora-server/#readme bug-reports: /~https://github.com/flora-pm/flora-server/issues author: Théophile Choutri diff --git a/src/core/Flora/Import/Package.hs b/src/core/Flora/Import/Package.hs index 81cc1f9a..9b031d46 100644 --- a/src/core/Flora/Import/Package.hs +++ b/src/core/Flora/Import/Package.hs @@ -125,7 +125,8 @@ coreLibraries = versionList :: Set Version versionList = Set.fromList - [ Version.mkVersion [9, 8, 1] + [ Version.mkVersion [9, 10, 1] + , Version.mkVersion [9, 8, 1] , Version.mkVersion [9, 6, 3] , Version.mkVersion [9, 6, 2] , Version.mkVersion [9, 6, 1]