mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Package manager UI-related tweaks (#4382)
* Add Plugins Path setting * Fix/improve cache invalidation * Hide load error when collapsing package source * Package manager style tweaks * Show error if installed packages query failed * Prevent "No packages found" flicker * Show <unknown> if empty version * Always show latest version, highlight if new version available * Fix issues with non-unique cross-source package ids * Don't wrap id, version and date * Decrease collapse button padding * Display description for scraper packages * Fix default packages population * Change default package path to community --------- Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
23b4d4f1e0
commit
a1bd7cf817
16 changed files with 611 additions and 500 deletions
|
|
@ -9,6 +9,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
|||
generatedPath
|
||||
metadataPath
|
||||
scrapersPath
|
||||
pluginsPath
|
||||
cachePath
|
||||
blobsPath
|
||||
blobsStorage
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ query InstalledPluginPackages {
|
|||
query InstalledPluginPackagesStatus {
|
||||
installedPackages(type: Plugin) {
|
||||
...PackageData
|
||||
upgrade {
|
||||
source_package {
|
||||
...PackageData
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ query InstalledScraperPackages {
|
|||
query InstalledScraperPackagesStatus {
|
||||
installedPackages(type: Scraper) {
|
||||
...PackageData
|
||||
upgrade {
|
||||
source_package {
|
||||
...PackageData
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ input ConfigGeneralInput {
|
|||
metadataPath: String
|
||||
"Path to scrapers"
|
||||
scrapersPath: String
|
||||
"Path to plugins"
|
||||
pluginsPath: String
|
||||
"Path to cache"
|
||||
cachePath: String
|
||||
"Path to blobs - required for filesystem blob storage"
|
||||
|
|
@ -189,6 +191,8 @@ type ConfigGeneralResult {
|
|||
configFilePath: String!
|
||||
"Path to scrapers"
|
||||
scrapersPath: String!
|
||||
"Path to plugins"
|
||||
pluginsPath: String!
|
||||
"Path to cache"
|
||||
cachePath: String!
|
||||
"Path to blobs - required for filesystem blob storage"
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ type Package {
|
|||
|
||||
sourceURL: String!
|
||||
|
||||
"The available upgraded version of this package"
|
||||
upgrade: Package
|
||||
"The version of this package currently available from the remote source"
|
||||
source_package: Package
|
||||
|
||||
metadata: Map!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
|||
}
|
||||
|
||||
refreshScraperCache := false
|
||||
refreshScraperSource := false
|
||||
existingScrapersPath := c.GetScrapersPath()
|
||||
if input.ScrapersPath != nil && existingScrapersPath != *input.ScrapersPath {
|
||||
if err := validateDir(config.ScrapersPath, *input.ScrapersPath, false); err != nil {
|
||||
|
|
@ -111,9 +112,23 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
|||
}
|
||||
|
||||
refreshScraperCache = true
|
||||
refreshScraperSource = true
|
||||
c.Set(config.ScrapersPath, input.ScrapersPath)
|
||||
}
|
||||
|
||||
refreshPluginCache := false
|
||||
refreshPluginSource := false
|
||||
existingPluginsPath := c.GetPluginsPath()
|
||||
if input.PluginsPath != nil && existingPluginsPath != *input.PluginsPath {
|
||||
if err := validateDir(config.PluginsPath, *input.PluginsPath, false); err != nil {
|
||||
return makeConfigGeneralResult(), err
|
||||
}
|
||||
|
||||
refreshPluginCache = true
|
||||
refreshPluginSource = true
|
||||
c.Set(config.PluginsPath, input.PluginsPath)
|
||||
}
|
||||
|
||||
existingMetadataPath := c.GetMetadataPath()
|
||||
if input.MetadataPath != nil && existingMetadataPath != *input.MetadataPath {
|
||||
if err := validateDir(config.Metadata, *input.MetadataPath, true); err != nil {
|
||||
|
|
@ -347,13 +362,11 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
|||
c.Set(config.DrawFunscriptHeatmapRange, input.DrawFunscriptHeatmapRange)
|
||||
}
|
||||
|
||||
refreshScraperSource := false
|
||||
if input.ScraperPackageSources != nil {
|
||||
c.Set(config.ScraperPackageSources, input.ScraperPackageSources)
|
||||
refreshScraperSource = true
|
||||
}
|
||||
|
||||
refreshPluginSource := false
|
||||
if input.PluginPackageSources != nil {
|
||||
c.Set(config.PluginPackageSources, input.PluginPackageSources)
|
||||
refreshPluginSource = true
|
||||
|
|
@ -367,6 +380,9 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
|||
if refreshScraperCache {
|
||||
manager.GetInstance().RefreshScraperCache()
|
||||
}
|
||||
if refreshPluginCache {
|
||||
manager.GetInstance().RefreshPluginCache()
|
||||
}
|
||||
if refreshStreamManager {
|
||||
manager.GetInstance().RefreshStreamManager()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
|
|||
MetadataPath: config.GetMetadataPath(),
|
||||
ConfigFilePath: config.GetConfigFile(),
|
||||
ScrapersPath: config.GetScrapersPath(),
|
||||
PluginsPath: config.GetPluginsPath(),
|
||||
CachePath: config.GetCachePath(),
|
||||
BlobsPath: config.GetBlobsPath(),
|
||||
BlobsStorage: config.GetBlobsStorage(),
|
||||
|
|
|
|||
|
|
@ -98,11 +98,24 @@ func sortedPackageSpecKeys[V any](m map[models.PackageSpecInput]V) []models.Pack
|
|||
}
|
||||
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
if strings.EqualFold(keys[i].ID, keys[j].ID) {
|
||||
return keys[i].ID < keys[j].ID
|
||||
a := keys[i]
|
||||
b := keys[j]
|
||||
|
||||
aID := a.ID
|
||||
bID := b.ID
|
||||
|
||||
if aID == bID {
|
||||
return a.SourceURL < b.SourceURL
|
||||
}
|
||||
|
||||
return strings.ToLower(keys[i].ID) < strings.ToLower(keys[j].ID)
|
||||
aIDL := strings.ToLower(aID)
|
||||
bIDL := strings.ToLower(bID)
|
||||
|
||||
if aIDL == bIDL {
|
||||
return aID < bID
|
||||
}
|
||||
|
||||
return aIDL < bIDL
|
||||
})
|
||||
|
||||
return keys
|
||||
|
|
@ -129,9 +142,9 @@ func (r *queryResolver) getInstalledPackagesWithUpgrades(ctx context.Context, pm
|
|||
for _, k := range sortedPackageSpecKeys(packageStatusIndex) {
|
||||
v := packageStatusIndex[k]
|
||||
p := manifestToPackage(*v.Local)
|
||||
if v.Upgradable() {
|
||||
if v.Remote != nil {
|
||||
pp := remotePackageToPackage(*v.Remote, allRemoteList)
|
||||
p.Upgrade = pp
|
||||
p.SourcePackage = pp
|
||||
}
|
||||
ret[i] = p
|
||||
i++
|
||||
|
|
@ -146,19 +159,19 @@ func (r *queryResolver) InstalledPackages(ctx context.Context, typeArg PackageTy
|
|||
return nil, err
|
||||
}
|
||||
|
||||
installed, err := pm.ListInstalled(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var ret []*Package
|
||||
|
||||
if sliceutil.Contains(graphql.CollectAllFields(ctx), "upgrade") {
|
||||
if sliceutil.Contains(graphql.CollectAllFields(ctx), "source_package") {
|
||||
ret, err = r.getInstalledPackagesWithUpgrades(ctx, pm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
installed, err := pm.ListInstalled(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret = make([]*Package, len(installed))
|
||||
i := 0
|
||||
for _, k := range sortedPackageSpecKeys(installed) {
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ const (
|
|||
PluginsSettingPrefix = PluginsSetting + "."
|
||||
DisabledPlugins = "plugins.disabled"
|
||||
|
||||
sourceDefaultPath = "stable"
|
||||
sourceDefaultPath = "community"
|
||||
sourceDefaultName = "Community (stable)"
|
||||
|
||||
PluginPackageSources = "plugins.package_sources"
|
||||
|
|
@ -1666,16 +1666,16 @@ func (i *Config) setDefaultValues() {
|
|||
i.main.SetDefault(NoProxy, noProxyDefault)
|
||||
|
||||
// set default package sources
|
||||
i.main.SetDefault(PluginPackageSources, map[string]string{
|
||||
i.main.SetDefault(PluginPackageSources, []map[string]string{{
|
||||
"name": sourceDefaultName,
|
||||
"url": pluginPackageSourcesDefault,
|
||||
"local_path": sourceDefaultPath,
|
||||
})
|
||||
i.main.SetDefault(ScraperPackageSources, map[string]string{
|
||||
"localpath": sourceDefaultPath,
|
||||
}})
|
||||
i.main.SetDefault(ScraperPackageSources, []map[string]string{{
|
||||
"name": sourceDefaultName,
|
||||
"url": scraperPackageSourcesDefault,
|
||||
"local_path": sourceDefaultPath,
|
||||
})
|
||||
"localpath": sourceDefaultPath,
|
||||
}})
|
||||
}
|
||||
|
||||
// setExistingSystemDefaults sets config options that are new and unset in an existing install,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import React, { useState } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
evictQueries,
|
||||
getClient,
|
||||
queryAvailablePluginPackages,
|
||||
useInstallPluginPackages,
|
||||
useInstalledPluginPackages,
|
||||
useInstalledPluginPackagesStatus,
|
||||
useUninstallPluginPackages,
|
||||
useUpdatePluginPackages,
|
||||
mutateInstallPluginPackages,
|
||||
mutateUninstallPluginPackages,
|
||||
mutateUpdatePluginPackages,
|
||||
pluginMutationImpactedQueries,
|
||||
} from "src/core/StashService";
|
||||
import { useMonitorJob } from "src/utils/job";
|
||||
import {
|
||||
|
|
@ -20,95 +20,59 @@ import { useSettings } from "./context";
|
|||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||
import { SettingSection } from "./SettingSection";
|
||||
|
||||
const impactedPackageChangeQueries = [
|
||||
GQL.PluginsDocument,
|
||||
GQL.PluginTasksDocument,
|
||||
GQL.InstalledPluginPackagesDocument,
|
||||
GQL.InstalledPluginPackagesStatusDocument,
|
||||
];
|
||||
|
||||
export const InstalledPluginPackages: React.FC = () => {
|
||||
const [loadUpgrades, setLoadUpgrades] = useState(false);
|
||||
const [jobID, setJobID] = useState<string>();
|
||||
const { job } = useMonitorJob(jobID, () => onPackageChanges());
|
||||
|
||||
const { data: installedPlugins, refetch: refetchPackages1 } =
|
||||
useInstalledPluginPackages({
|
||||
skip: loadUpgrades,
|
||||
});
|
||||
|
||||
const {
|
||||
data: withStatus,
|
||||
refetch: refetchPackages2,
|
||||
loading: statusLoading,
|
||||
} = useInstalledPluginPackagesStatus({
|
||||
skip: !loadUpgrades,
|
||||
});
|
||||
|
||||
const [updatePackages] = useUpdatePluginPackages();
|
||||
const [uninstallPackages] = useUninstallPluginPackages();
|
||||
const { data, previousData, refetch, loading, error } =
|
||||
useInstalledPluginPackages(loadUpgrades);
|
||||
|
||||
async function onUpdatePackages(packages: GQL.PackageSpecInput[]) {
|
||||
const r = await updatePackages({
|
||||
variables: {
|
||||
packages,
|
||||
},
|
||||
});
|
||||
const r = await mutateUpdatePluginPackages(packages);
|
||||
|
||||
setJobID(r.data?.updatePackages);
|
||||
}
|
||||
|
||||
async function onUninstallPackages(packages: GQL.PackageSpecInput[]) {
|
||||
const r = await uninstallPackages({
|
||||
variables: {
|
||||
packages,
|
||||
},
|
||||
});
|
||||
const r = await mutateUninstallPluginPackages(packages);
|
||||
|
||||
setJobID(r.data?.uninstallPackages);
|
||||
}
|
||||
|
||||
function refetchPackages() {
|
||||
refetchPackages1();
|
||||
refetchPackages2();
|
||||
}
|
||||
|
||||
function onPackageChanges() {
|
||||
// job is complete, refresh all local data
|
||||
const ac = getClient();
|
||||
evictQueries(ac.cache, impactedPackageChangeQueries);
|
||||
evictQueries(ac.cache, pluginMutationImpactedQueries);
|
||||
}
|
||||
|
||||
function onCheckForUpdates() {
|
||||
if (!loadUpgrades) {
|
||||
setLoadUpgrades(true);
|
||||
} else {
|
||||
refetchPackages();
|
||||
refetch();
|
||||
}
|
||||
}
|
||||
|
||||
const installedPackages = useMemo(() => {
|
||||
if (withStatus?.installedPackages) {
|
||||
return withStatus.installedPackages;
|
||||
}
|
||||
|
||||
return installedPlugins?.installedPackages ?? [];
|
||||
}, [installedPlugins, withStatus]);
|
||||
|
||||
const loading = !!job || statusLoading;
|
||||
// when loadUpgrades changes from false to true, data is set to undefined while the request is loading
|
||||
// so use previousData as a fallback, which will be the result when loadUpgrades was false,
|
||||
// to prevent displaying a "No packages found" message
|
||||
const installedPackages =
|
||||
data?.installedPackages ?? previousData?.installedPackages ?? [];
|
||||
|
||||
return (
|
||||
<SettingSection headingID="config.plugins.installed_plugins">
|
||||
<div className="package-manager">
|
||||
<InstalledPackages
|
||||
loading={loading}
|
||||
loading={!!job || loading}
|
||||
error={error?.message}
|
||||
packages={installedPackages}
|
||||
onCheckForUpdates={onCheckForUpdates}
|
||||
onUpdatePackages={(packages) =>
|
||||
onUpdatePackages(
|
||||
packages.map((p) => ({
|
||||
id: p.package_id,
|
||||
sourceURL: p.upgrade!.sourceURL,
|
||||
sourceURL: p.sourceURL,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
|
@ -120,7 +84,7 @@ export const InstalledPluginPackages: React.FC = () => {
|
|||
}))
|
||||
)
|
||||
}
|
||||
updatesLoaded={loadUpgrades}
|
||||
updatesLoaded={loadUpgrades && !loading}
|
||||
/>
|
||||
</div>
|
||||
</SettingSection>
|
||||
|
|
@ -130,18 +94,11 @@ export const InstalledPluginPackages: React.FC = () => {
|
|||
export const AvailablePluginPackages: React.FC = () => {
|
||||
const { general, loading: configLoading, error, saveGeneral } = useSettings();
|
||||
|
||||
const [sources, setSources] = useState<GQL.PackageSource[]>();
|
||||
const [jobID, setJobID] = useState<string>();
|
||||
const { job } = useMonitorJob(jobID, () => onPackageChanges());
|
||||
|
||||
const [installPackages] = useInstallPluginPackages();
|
||||
|
||||
async function onInstallPackages(packages: GQL.PackageSpecInput[]) {
|
||||
const r = await installPackages({
|
||||
variables: {
|
||||
packages,
|
||||
},
|
||||
});
|
||||
const r = await mutateInstallPluginPackages(packages);
|
||||
|
||||
setJobID(r.data?.installPackages);
|
||||
}
|
||||
|
|
@ -149,15 +106,9 @@ export const AvailablePluginPackages: React.FC = () => {
|
|||
function onPackageChanges() {
|
||||
// job is complete, refresh all local data
|
||||
const ac = getClient();
|
||||
evictQueries(ac.cache, impactedPackageChangeQueries);
|
||||
evictQueries(ac.cache, pluginMutationImpactedQueries);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!sources && !configLoading && general.pluginPackageSources) {
|
||||
setSources(general.pluginPackageSources);
|
||||
}
|
||||
}, [sources, configLoading, general.pluginPackageSources]);
|
||||
|
||||
async function loadSource(source: string): Promise<RemotePackage[]> {
|
||||
const { data } = await queryAvailablePluginPackages(source);
|
||||
return data.availablePackages;
|
||||
|
|
@ -167,10 +118,6 @@ export const AvailablePluginPackages: React.FC = () => {
|
|||
saveGeneral({
|
||||
pluginPackageSources: [...(general.pluginPackageSources ?? []), source],
|
||||
});
|
||||
|
||||
setSources((prev) => {
|
||||
return [...(prev ?? []), source];
|
||||
});
|
||||
}
|
||||
|
||||
function editSource(existing: GQL.PackageSource, changed: GQL.PackageSource) {
|
||||
|
|
@ -179,10 +126,6 @@ export const AvailablePluginPackages: React.FC = () => {
|
|||
s.url === existing.url ? changed : s
|
||||
),
|
||||
});
|
||||
|
||||
setSources((prev) => {
|
||||
return prev?.map((s) => (s.url === existing.url ? changed : s));
|
||||
});
|
||||
}
|
||||
|
||||
function deleteSource(source: GQL.PackageSource) {
|
||||
|
|
@ -191,10 +134,6 @@ export const AvailablePluginPackages: React.FC = () => {
|
|||
(s) => s.url !== source.url
|
||||
),
|
||||
});
|
||||
|
||||
setSources((prev) => {
|
||||
return prev?.filter((s) => s.url !== source.url);
|
||||
});
|
||||
}
|
||||
|
||||
function renderDescription(pkg: RemotePackage) {
|
||||
|
|
@ -208,6 +147,8 @@ export const AvailablePluginPackages: React.FC = () => {
|
|||
|
||||
const loading = !!job;
|
||||
|
||||
const sources = general?.pluginPackageSources ?? [];
|
||||
|
||||
return (
|
||||
<SettingSection headingID="config.plugins.available_plugins">
|
||||
<div className="package-manager">
|
||||
|
|
@ -216,7 +157,7 @@ export const AvailablePluginPackages: React.FC = () => {
|
|||
onInstallPackages={onInstallPackages}
|
||||
renderDescription={renderDescription}
|
||||
loadSource={(source) => loadSource(source)}
|
||||
sources={sources ?? []}
|
||||
sources={sources}
|
||||
addSource={addSource}
|
||||
editSource={editSource}
|
||||
deleteSource={deleteSource}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import React, { useState } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
evictQueries,
|
||||
getClient,
|
||||
queryAvailableScraperPackages,
|
||||
useInstallScraperPackages,
|
||||
useInstalledScraperPackages,
|
||||
useInstalledScraperPackagesStatus,
|
||||
useUninstallScraperPackages,
|
||||
useUpdateScraperPackages,
|
||||
mutateUpdateScraperPackages,
|
||||
mutateUninstallScraperPackages,
|
||||
mutateInstallScraperPackages,
|
||||
scraperMutationImpactedQueries,
|
||||
} from "src/core/StashService";
|
||||
import { useMonitorJob } from "src/utils/job";
|
||||
import {
|
||||
|
|
@ -20,96 +20,59 @@ import { useSettings } from "./context";
|
|||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||
import { SettingSection } from "./SettingSection";
|
||||
|
||||
const impactedPackageChangeQueries = [
|
||||
GQL.ListPerformerScrapersDocument,
|
||||
GQL.ListSceneScrapersDocument,
|
||||
GQL.ListMovieScrapersDocument,
|
||||
GQL.InstalledScraperPackagesDocument,
|
||||
GQL.InstalledScraperPackagesStatusDocument,
|
||||
];
|
||||
|
||||
export const InstalledScraperPackages: React.FC = () => {
|
||||
const [loadUpgrades, setLoadUpgrades] = useState(false);
|
||||
const [jobID, setJobID] = useState<string>();
|
||||
const { job } = useMonitorJob(jobID, () => onPackageChanges());
|
||||
|
||||
const { data: installedScrapers, refetch: refetchPackages1 } =
|
||||
useInstalledScraperPackages({
|
||||
skip: loadUpgrades,
|
||||
});
|
||||
|
||||
const {
|
||||
data: withStatus,
|
||||
refetch: refetchPackages2,
|
||||
loading: statusLoading,
|
||||
} = useInstalledScraperPackagesStatus({
|
||||
skip: !loadUpgrades,
|
||||
});
|
||||
|
||||
const [updatePackages] = useUpdateScraperPackages();
|
||||
const [uninstallPackages] = useUninstallScraperPackages();
|
||||
const { data, previousData, refetch, loading, error } =
|
||||
useInstalledScraperPackages(loadUpgrades);
|
||||
|
||||
async function onUpdatePackages(packages: GQL.PackageSpecInput[]) {
|
||||
const r = await updatePackages({
|
||||
variables: {
|
||||
packages,
|
||||
},
|
||||
});
|
||||
const r = await mutateUpdateScraperPackages(packages);
|
||||
|
||||
setJobID(r.data?.updatePackages);
|
||||
}
|
||||
|
||||
async function onUninstallPackages(packages: GQL.PackageSpecInput[]) {
|
||||
const r = await uninstallPackages({
|
||||
variables: {
|
||||
packages,
|
||||
},
|
||||
});
|
||||
const r = await mutateUninstallScraperPackages(packages);
|
||||
|
||||
setJobID(r.data?.uninstallPackages);
|
||||
}
|
||||
|
||||
function refetchPackages() {
|
||||
refetchPackages1();
|
||||
refetchPackages2();
|
||||
}
|
||||
|
||||
function onPackageChanges() {
|
||||
// job is complete, refresh all local data
|
||||
const ac = getClient();
|
||||
evictQueries(ac.cache, impactedPackageChangeQueries);
|
||||
evictQueries(ac.cache, scraperMutationImpactedQueries);
|
||||
}
|
||||
|
||||
function onCheckForUpdates() {
|
||||
if (!loadUpgrades) {
|
||||
setLoadUpgrades(true);
|
||||
} else {
|
||||
refetchPackages();
|
||||
refetch();
|
||||
}
|
||||
}
|
||||
|
||||
const installedPackages = useMemo(() => {
|
||||
if (withStatus?.installedPackages) {
|
||||
return withStatus.installedPackages;
|
||||
}
|
||||
|
||||
return installedScrapers?.installedPackages ?? [];
|
||||
}, [installedScrapers, withStatus]);
|
||||
|
||||
const loading = !!job || statusLoading;
|
||||
// when loadUpgrades changes from false to true, data is set to undefined while the request is loading
|
||||
// so use previousData as a fallback, which will be the result when loadUpgrades was false,
|
||||
// to prevent displaying a "No packages found" message
|
||||
const installedPackages =
|
||||
data?.installedPackages ?? previousData?.installedPackages ?? [];
|
||||
|
||||
return (
|
||||
<SettingSection headingID="config.scraping.installed_scrapers">
|
||||
<div className="package-manager">
|
||||
<InstalledPackages
|
||||
loading={loading}
|
||||
loading={!!job || loading}
|
||||
error={error?.message}
|
||||
packages={installedPackages}
|
||||
onCheckForUpdates={onCheckForUpdates}
|
||||
onUpdatePackages={(packages) =>
|
||||
onUpdatePackages(
|
||||
packages.map((p) => ({
|
||||
id: p.package_id,
|
||||
sourceURL: p.upgrade!.sourceURL,
|
||||
sourceURL: p.sourceURL,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
|
@ -121,7 +84,7 @@ export const InstalledScraperPackages: React.FC = () => {
|
|||
}))
|
||||
)
|
||||
}
|
||||
updatesLoaded={loadUpgrades}
|
||||
updatesLoaded={loadUpgrades && !loading}
|
||||
/>
|
||||
</div>
|
||||
</SettingSection>
|
||||
|
|
@ -131,18 +94,11 @@ export const InstalledScraperPackages: React.FC = () => {
|
|||
export const AvailableScraperPackages: React.FC = () => {
|
||||
const { general, loading: configLoading, error, saveGeneral } = useSettings();
|
||||
|
||||
const [sources, setSources] = useState<GQL.PackageSource[]>();
|
||||
const [jobID, setJobID] = useState<string>();
|
||||
const { job } = useMonitorJob(jobID, () => onPackageChanges());
|
||||
|
||||
const [installPackages] = useInstallScraperPackages();
|
||||
|
||||
async function onInstallPackages(packages: GQL.PackageSpecInput[]) {
|
||||
const r = await installPackages({
|
||||
variables: {
|
||||
packages,
|
||||
},
|
||||
});
|
||||
const r = await mutateInstallScraperPackages(packages);
|
||||
|
||||
setJobID(r.data?.installPackages);
|
||||
}
|
||||
|
|
@ -150,15 +106,9 @@ export const AvailableScraperPackages: React.FC = () => {
|
|||
function onPackageChanges() {
|
||||
// job is complete, refresh all local data
|
||||
const ac = getClient();
|
||||
evictQueries(ac.cache, impactedPackageChangeQueries);
|
||||
evictQueries(ac.cache, scraperMutationImpactedQueries);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!sources && !configLoading && general.scraperPackageSources) {
|
||||
setSources(general.scraperPackageSources);
|
||||
}
|
||||
}, [sources, configLoading, general.scraperPackageSources]);
|
||||
|
||||
async function loadSource(source: string): Promise<RemotePackage[]> {
|
||||
const { data } = await queryAvailableScraperPackages(source);
|
||||
return data.availablePackages;
|
||||
|
|
@ -168,10 +118,6 @@ export const AvailableScraperPackages: React.FC = () => {
|
|||
saveGeneral({
|
||||
scraperPackageSources: [...(general.scraperPackageSources ?? []), source],
|
||||
});
|
||||
|
||||
setSources((prev) => {
|
||||
return [...(prev ?? []), source];
|
||||
});
|
||||
}
|
||||
|
||||
function editSource(existing: GQL.PackageSource, changed: GQL.PackageSource) {
|
||||
|
|
@ -180,10 +126,6 @@ export const AvailableScraperPackages: React.FC = () => {
|
|||
s.url === existing.url ? changed : s
|
||||
),
|
||||
});
|
||||
|
||||
setSources((prev) => {
|
||||
return prev?.map((s) => (s.url === existing.url ? changed : s));
|
||||
});
|
||||
}
|
||||
|
||||
function deleteSource(source: GQL.PackageSource) {
|
||||
|
|
@ -192,10 +134,12 @@ export const AvailableScraperPackages: React.FC = () => {
|
|||
(s) => s.url !== source.url
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
setSources((prev) => {
|
||||
return prev?.filter((s) => s.url !== source.url);
|
||||
});
|
||||
function renderDescription(pkg: RemotePackage) {
|
||||
if (pkg.metadata.description) {
|
||||
return pkg.metadata.description;
|
||||
}
|
||||
}
|
||||
|
||||
if (error) return <h1>{error.message}</h1>;
|
||||
|
|
@ -203,14 +147,17 @@ export const AvailableScraperPackages: React.FC = () => {
|
|||
|
||||
const loading = !!job;
|
||||
|
||||
const sources = general?.scraperPackageSources ?? [];
|
||||
|
||||
return (
|
||||
<SettingSection headingID="config.scraping.available_scrapers">
|
||||
<div className="package-manager">
|
||||
<AvailablePackages
|
||||
loading={loading}
|
||||
onInstallPackages={onInstallPackages}
|
||||
renderDescription={renderDescription}
|
||||
loadSource={(source) => loadSource(source)}
|
||||
sources={sources ?? []}
|
||||
sources={sources}
|
||||
addSource={addSource}
|
||||
editSource={editSource}
|
||||
deleteSource={deleteSource}
|
||||
|
|
|
|||
|
|
@ -137,6 +137,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||
onChange={(v) => saveGeneral({ scrapersPath: v })}
|
||||
/>
|
||||
|
||||
<StringSetting
|
||||
id="plugins-path"
|
||||
headingID="config.general.plugins_path.heading"
|
||||
subHeadingID="config.general.plugins_path.description"
|
||||
value={general.pluginsPath ?? undefined}
|
||||
onChange={(v) => saveGeneral({ pluginsPath: v })}
|
||||
/>
|
||||
|
||||
<StringSetting
|
||||
id="metadata-path"
|
||||
headingID="config.general.metadata_path.heading"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import React, { useState, useMemo, useEffect } from "react";
|
|||
import { FormattedMessage, IntlShape, useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Icon } from "../Icon";
|
||||
import cx from "classnames";
|
||||
import {
|
||||
faAnglesUp,
|
||||
faChevronDown,
|
||||
faChevronRight,
|
||||
faRotate,
|
||||
|
|
@ -17,7 +19,19 @@ import { LoadingIndicator } from "../LoadingIndicator";
|
|||
import { ApolloError } from "@apollo/client";
|
||||
import { ClearableInput } from "../ClearableInput";
|
||||
|
||||
function formatDate(intl: IntlShape, date: string | undefined | null) {
|
||||
function packageKey(
|
||||
pkg: Pick<GQL.Package, "package_id" | "sourceURL">
|
||||
): string {
|
||||
return `${pkg.sourceURL}-${pkg.package_id}`;
|
||||
}
|
||||
|
||||
function displayVersion(intl: IntlShape, version: string | undefined | null) {
|
||||
if (!version) return intl.formatMessage({ id: "package_manager.unknown" });
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
function displayDate(intl: IntlShape, date: string | undefined | null) {
|
||||
if (!date) return;
|
||||
|
||||
const d = new Date(date);
|
||||
|
|
@ -59,14 +73,17 @@ const InstalledPackageRow: React.FC<{
|
|||
}> = ({ loading, pkg, selected, togglePackage, updatesLoaded }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
function rowClassname() {
|
||||
if (pkg.upgrade?.version) {
|
||||
return "package-update-available";
|
||||
}
|
||||
}
|
||||
const updateAvailable = useMemo(() => {
|
||||
if (!updatesLoaded) return false;
|
||||
if (!pkg.date || !pkg.source_package?.date) return false;
|
||||
|
||||
const pkgDate = new Date(pkg.date);
|
||||
const upgradeDate = new Date(pkg.source_package.date);
|
||||
return upgradeDate > pkgDate;
|
||||
}, [updatesLoaded, pkg]);
|
||||
|
||||
return (
|
||||
<tr className={rowClassname()}>
|
||||
<tr className={cx({ "package-update-available": updateAvailable })}>
|
||||
<td>
|
||||
<Form.Check
|
||||
checked={selected}
|
||||
|
|
@ -79,17 +96,22 @@ const InstalledPackageRow: React.FC<{
|
|||
<span className="package-id">{pkg.package_id}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="package-version">{pkg.version}</span>
|
||||
<span className="package-date">{formatDate(intl, pkg.date)}</span>
|
||||
<span className="package-version">
|
||||
{displayVersion(intl, pkg.version)}
|
||||
</span>
|
||||
<span className="package-date">{displayDate(intl, pkg.date)}</span>
|
||||
</td>
|
||||
{updatesLoaded ? (
|
||||
{updatesLoaded && pkg.source_package && (
|
||||
<td>
|
||||
<span className="package-version">{pkg.upgrade?.version}</span>
|
||||
<span className="package-date">
|
||||
{formatDate(intl, pkg.upgrade?.date)}
|
||||
<span className="package-latest-version">
|
||||
{displayVersion(intl, pkg.source_package.version)}
|
||||
{updateAvailable && <Icon icon={faAnglesUp} />}
|
||||
</span>
|
||||
<span className="package-latest-date">
|
||||
{displayDate(intl, pkg.source_package.date)}
|
||||
</span>
|
||||
</td>
|
||||
) : undefined}
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
|
@ -97,6 +119,7 @@ const InstalledPackageRow: React.FC<{
|
|||
const InstalledPackagesList: React.FC<{
|
||||
filter: string;
|
||||
loading?: boolean;
|
||||
error?: string;
|
||||
updatesLoaded: boolean;
|
||||
packages: InstalledPackage[];
|
||||
checkedPackages: InstalledPackage[];
|
||||
|
|
@ -108,12 +131,13 @@ const InstalledPackagesList: React.FC<{
|
|||
setCheckedPackages,
|
||||
updatesLoaded,
|
||||
loading,
|
||||
error,
|
||||
}) => {
|
||||
const checkedMap = useMemo(() => {
|
||||
const map: Record<string, boolean> = {};
|
||||
checkedPackages.forEach((pkg) => {
|
||||
map[`${pkg.sourceURL}-${pkg.package_id}`] = true;
|
||||
});
|
||||
for (const pkg of checkedPackages) {
|
||||
map[packageKey(pkg)] = true;
|
||||
}
|
||||
return map;
|
||||
}, [checkedPackages]);
|
||||
|
||||
|
|
@ -134,19 +158,54 @@ const InstalledPackagesList: React.FC<{
|
|||
|
||||
setCheckedPackages((prev) => {
|
||||
if (prev.includes(pkg)) {
|
||||
return prev.filter((n) => n.package_id !== pkg.package_id);
|
||||
return prev.filter((n) => packageKey(n) !== packageKey(pkg));
|
||||
} else {
|
||||
return prev.concat(pkg);
|
||||
return [...prev, pkg];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderBody() {
|
||||
if (error) {
|
||||
return (
|
||||
<tr>
|
||||
<td />
|
||||
<td colSpan={1000} className="source-error">
|
||||
<Icon icon={faWarning} />
|
||||
<span>{error}</span>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredPackages.length === 0) {
|
||||
return (
|
||||
<tr className="package-manager-no-results">
|
||||
<td colSpan={1000}>
|
||||
<FormattedMessage id="package_manager.no_packages" />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return filteredPackages.map((pkg) => (
|
||||
<InstalledPackageRow
|
||||
key={packageKey(pkg)}
|
||||
loading={loading}
|
||||
pkg={pkg}
|
||||
selected={checkedMap[packageKey(pkg)] ?? false}
|
||||
togglePackage={() => togglePackage(pkg)}
|
||||
updatesLoaded={updatesLoaded}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="package-manager-table-container">
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="button-cell">
|
||||
<th className="check-cell">
|
||||
<Form.Check
|
||||
checked={allChecked ?? false}
|
||||
onChange={toggleAllChecked}
|
||||
|
|
@ -166,28 +225,7 @@ const InstalledPackagesList: React.FC<{
|
|||
) : undefined}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredPackages.length === 0 ? (
|
||||
<tr className="package-manager-no-results">
|
||||
<td colSpan={updatesLoaded ? 4 : 3}>
|
||||
<FormattedMessage id="package_manager.no_packages" />
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredPackages.map((pkg) => (
|
||||
<InstalledPackageRow
|
||||
key={`${pkg.sourceURL}-${pkg.package_id}`}
|
||||
loading={loading}
|
||||
pkg={pkg}
|
||||
selected={
|
||||
checkedMap[`${pkg.sourceURL}-${pkg.package_id}`] ?? false
|
||||
}
|
||||
togglePackage={() => togglePackage(pkg)}
|
||||
updatesLoaded={updatesLoaded}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
<tbody>{renderBody()}</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -213,14 +251,12 @@ const InstalledPackagesToolbar: React.FC<{
|
|||
const intl = useIntl();
|
||||
return (
|
||||
<div className="package-manager-toolbar">
|
||||
<div>
|
||||
<ClearableInput
|
||||
placeholder={`${intl.formatMessage({ id: "filter" })}...`}
|
||||
value={filter}
|
||||
setValue={(v) => setFilter(v)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex-grow-1" />
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => onCheckForUpdates()}
|
||||
|
|
@ -243,12 +279,12 @@ const InstalledPackagesToolbar: React.FC<{
|
|||
<FormattedMessage id="package_manager.uninstall" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InstalledPackages: React.FC<{
|
||||
loading?: boolean;
|
||||
error?: string;
|
||||
packages: InstalledPackage[];
|
||||
updatesLoaded: boolean;
|
||||
onCheckForUpdates: () => void;
|
||||
|
|
@ -261,6 +297,7 @@ export const InstalledPackages: React.FC<{
|
|||
onUpdatePackages,
|
||||
onUninstallPackages,
|
||||
loading,
|
||||
error,
|
||||
}) => {
|
||||
const [checkedPackages, setCheckedPackages] = useState<InstalledPackage[]>(
|
||||
[]
|
||||
|
|
@ -275,7 +312,7 @@ export const InstalledPackages: React.FC<{
|
|||
useEffect(() => {
|
||||
setCheckedPackages((prev) => {
|
||||
const newVal = prev.filter((pkg) =>
|
||||
packages.find((p) => p.package_id === pkg.package_id)
|
||||
packages.find((p) => packageKey(p) === packageKey(pkg))
|
||||
);
|
||||
if (newVal.length !== prev.length) {
|
||||
return newVal;
|
||||
|
|
@ -316,6 +353,7 @@ export const InstalledPackages: React.FC<{
|
|||
<InstalledPackagesList
|
||||
filter={filter}
|
||||
loading={loading}
|
||||
error={error}
|
||||
packages={packages}
|
||||
// use original checked packages so that check boxes are not affected by filter
|
||||
checkedPackages={checkedPackages}
|
||||
|
|
@ -353,7 +391,6 @@ const AvailablePackagesToolbar: React.FC<{
|
|||
|
||||
return (
|
||||
<div className="package-manager-toolbar">
|
||||
<div>
|
||||
<ClearableInput
|
||||
placeholder={`${intl.formatMessage({ id: "filter" })}...`}
|
||||
value={filter}
|
||||
|
|
@ -368,8 +405,7 @@ const AvailablePackagesToolbar: React.FC<{
|
|||
<FormattedMessage id={selectedOnlyId} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex-grow-1" />
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!hasSelectedPackages || loading}
|
||||
|
|
@ -378,7 +414,6 @@ const AvailablePackagesToolbar: React.FC<{
|
|||
<FormattedMessage id="package_manager.install" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -552,7 +587,7 @@ const AvailablePackageRow: React.FC<{
|
|||
}
|
||||
|
||||
return (
|
||||
<tr key={pkg.package_id}>
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
<Form.Check
|
||||
checked={selected ?? false}
|
||||
|
|
@ -565,8 +600,10 @@ const AvailablePackageRow: React.FC<{
|
|||
<span className="package-id">{pkg.package_id}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span className="package-version">{pkg.version}</span>
|
||||
<span className="package-date">{formatDate(intl, pkg.date)}</span>
|
||||
<span className="package-version">
|
||||
{displayVersion(intl, pkg.version)}
|
||||
</span>
|
||||
<span className="package-date">{displayDate(intl, pkg.date)}</span>
|
||||
</td>
|
||||
<td>
|
||||
{renderRequiredBy()}
|
||||
|
|
@ -655,36 +692,62 @@ const SourcePackagesList: React.FC<{
|
|||
}
|
||||
|
||||
function toggleSourceOpen() {
|
||||
if (sourceOpen) {
|
||||
setLoadError(undefined);
|
||||
setSourceOpen(false);
|
||||
} else {
|
||||
if (packages === undefined) {
|
||||
loadPackages();
|
||||
}
|
||||
|
||||
setSourceOpen((prev) => !prev);
|
||||
setSourceOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
function renderCollapseButton() {
|
||||
function renderContents() {
|
||||
if (loading) {
|
||||
return (
|
||||
<Button
|
||||
variant="minimal"
|
||||
size="sm"
|
||||
className="package-collapse-button"
|
||||
onClick={() => toggleSourceOpen()}
|
||||
>
|
||||
<Icon icon={sourceOpen ? faChevronDown : faChevronRight} />
|
||||
</Button>
|
||||
<tr>
|
||||
<td colSpan={2}></td>
|
||||
<td colSpan={3}>
|
||||
<LoadingIndicator inline small />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
const children = useMemo(() => {
|
||||
if (loadError) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={2}></td>
|
||||
<td colSpan={3} className="source-error">
|
||||
<Icon icon={faWarning} />
|
||||
<span>{loadError}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => loadPackages()}
|
||||
title={intl.formatMessage({ id: "actions.reload" })}
|
||||
>
|
||||
<Icon icon={faRotate} />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
if (!sourceOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function getRequiredPackages(pkg: RemotePackage) {
|
||||
const ret: RemotePackage[] = [];
|
||||
pkg.requires.forEach((r) => {
|
||||
for (const r of pkg.requires) {
|
||||
const found = packages?.find((p) => p.package_id === r.package_id);
|
||||
if (found && !ret.includes(found)) {
|
||||
ret.push(found);
|
||||
ret.push(...getRequiredPackages(found));
|
||||
}
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
@ -698,10 +761,7 @@ const SourcePackagesList: React.FC<{
|
|||
return prev.filter((n) => n.package_id !== pkg.package_id);
|
||||
} else {
|
||||
// also include required packages
|
||||
const toAdd = [pkg];
|
||||
toAdd.push(...getRequiredPackages(pkg));
|
||||
|
||||
return prev.concat(...toAdd);
|
||||
return [...prev, pkg, ...getRequiredPackages(pkg)];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -711,27 +771,19 @@ const SourcePackagesList: React.FC<{
|
|||
key={pkg.package_id}
|
||||
disabled={disabled}
|
||||
pkg={pkg}
|
||||
requiredBy={selectedPackages.filter((p) => {
|
||||
return p.requires.find((r) => r.package_id === pkg.package_id);
|
||||
})}
|
||||
requiredBy={selectedPackages.filter((p) =>
|
||||
p.requires.some((r) => r.package_id === pkg.package_id)
|
||||
)}
|
||||
selected={checkedMap[pkg.package_id] ?? false}
|
||||
togglePackage={() => togglePackage(pkg)}
|
||||
renderDescription={renderDescription}
|
||||
/>
|
||||
));
|
||||
}, [
|
||||
filteredPackages,
|
||||
disabled,
|
||||
checkedMap,
|
||||
selectedPackages,
|
||||
setSelectedPackages,
|
||||
packages,
|
||||
renderDescription,
|
||||
]);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr key={source.url} className="package-source">
|
||||
<tr className="package-source">
|
||||
<td>
|
||||
{packages !== undefined ? (
|
||||
<Form.Check
|
||||
|
|
@ -741,8 +793,20 @@ const SourcePackagesList: React.FC<{
|
|||
/>
|
||||
) : undefined}
|
||||
</td>
|
||||
<td>{renderCollapseButton()}</td>
|
||||
<td colSpan={2} onClick={() => toggleSourceOpen()}>
|
||||
<td className="source-collapse">
|
||||
<Button
|
||||
variant="minimal"
|
||||
size="sm"
|
||||
onClick={() => toggleSourceOpen()}
|
||||
>
|
||||
<Icon icon={sourceOpen ? faChevronDown : faChevronRight} />
|
||||
</Button>
|
||||
</td>
|
||||
<td
|
||||
className="source-name"
|
||||
colSpan={2}
|
||||
onClick={() => toggleSourceOpen()}
|
||||
>
|
||||
<span>{source.name ?? source.url}</span>
|
||||
</td>
|
||||
<td className="source-controls">
|
||||
|
|
@ -764,32 +828,7 @@ const SourcePackagesList: React.FC<{
|
|||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={2}></td>
|
||||
<td colSpan={3}>
|
||||
<LoadingIndicator inline small />
|
||||
</td>
|
||||
</tr>
|
||||
) : undefined}
|
||||
{loadError ? (
|
||||
<tr>
|
||||
<td colSpan={2}></td>
|
||||
<td colSpan={3} className="source-error">
|
||||
<Icon icon={faWarning} />
|
||||
<span>{loadError}</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => loadPackages()}
|
||||
title={intl.formatMessage({ id: "actions.reload" })}
|
||||
>
|
||||
<Icon icon={faRotate} />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
) : undefined}
|
||||
{sourceOpen && !loading && children}
|
||||
{renderContents()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -847,6 +886,58 @@ const AvailablePackagesList: React.FC<{
|
|||
});
|
||||
}
|
||||
|
||||
function renderBody() {
|
||||
if (sources.length === 0) {
|
||||
return (
|
||||
<tr className="package-manager-no-results">
|
||||
<td colSpan={5}>
|
||||
<FormattedMessage id="package_manager.no_sources" />
|
||||
<br />
|
||||
<Button
|
||||
size="sm"
|
||||
variant="success"
|
||||
onClick={() => setAddingSource(true)}
|
||||
>
|
||||
<FormattedMessage id="package_manager.add_source" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{sources.map((src) => (
|
||||
<SourcePackagesList
|
||||
key={src.url}
|
||||
filter={filter}
|
||||
disabled={loading}
|
||||
source={src}
|
||||
renderDescription={renderDescription}
|
||||
loadSource={() => loadSource(src.url)}
|
||||
selectedOnly={selectedOnly}
|
||||
selectedPackages={selectedPackages[src.url] ?? []}
|
||||
setSelectedPackages={(v) => setSelectedSourcePackages(src, v)}
|
||||
editSource={() => setEditingSource(src)}
|
||||
deleteSource={() => setDeletingSource(src)}
|
||||
/>
|
||||
))}
|
||||
<tr className="add-package-source">
|
||||
<td colSpan={2}></td>
|
||||
<td colSpan={3}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="success"
|
||||
onClick={() => setAddingSource(true)}
|
||||
>
|
||||
<FormattedMessage id="package_manager.add_source" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
|
|
@ -880,8 +971,8 @@ const AvailablePackagesList: React.FC<{
|
|||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="button-cell"></th>
|
||||
<th className="button-cell"></th>
|
||||
<th className="check-cell"></th>
|
||||
<th className="collapse-cell"></th>
|
||||
<th>
|
||||
<FormattedMessage id="package_manager.package" />
|
||||
</th>
|
||||
|
|
@ -893,49 +984,7 @@ const AvailablePackagesList: React.FC<{
|
|||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{sources.length === 0 ? (
|
||||
<tr className="package-manager-no-results">
|
||||
<td colSpan={5}>
|
||||
<FormattedMessage id="package_manager.no_sources" />
|
||||
<br />
|
||||
<Button
|
||||
size="sm"
|
||||
variant="success"
|
||||
onClick={() => setAddingSource(true)}
|
||||
>
|
||||
<FormattedMessage id="package_manager.add_source" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
sources.map((src) => (
|
||||
<SourcePackagesList
|
||||
key={src.url}
|
||||
filter={filter}
|
||||
disabled={loading}
|
||||
source={src}
|
||||
renderDescription={renderDescription}
|
||||
loadSource={() => loadSource(src.url)}
|
||||
selectedOnly={selectedOnly}
|
||||
selectedPackages={selectedPackages[src.url] ?? []}
|
||||
setSelectedPackages={(v) => setSelectedSourcePackages(src, v)}
|
||||
editSource={() => setEditingSource(src)}
|
||||
deleteSource={() => setDeletingSource(src)}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
{sources.length > 0 ? (
|
||||
<tr className="package-source">
|
||||
<td colSpan={2}></td>
|
||||
<td colSpan={3} onClick={() => setAddingSource(true)}>
|
||||
<Button size="sm" variant="success">
|
||||
<FormattedMessage id="package_manager.add_source" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
) : undefined}
|
||||
</tbody>
|
||||
<tbody>{renderBody()}</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,19 @@
|
|||
.package-source {
|
||||
font-weight: bold;
|
||||
|
||||
.source-collapse {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
|
||||
.btn {
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.source-controls {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,10 +26,6 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.package-collapse-button {
|
||||
color: $text-color;
|
||||
}
|
||||
|
||||
.package-manager-table-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
|
|
@ -31,39 +37,54 @@
|
|||
top: 0;
|
||||
z-index: 1;
|
||||
|
||||
.button-cell {
|
||||
.check-cell {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.collapse-cell {
|
||||
width: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.package-name,
|
||||
.package-id,
|
||||
.package-version,
|
||||
.package-date,
|
||||
.package-name,
|
||||
.package-id {
|
||||
.package-latest-version,
|
||||
.package-latest-date {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.package-id,
|
||||
.package-date,
|
||||
.package-id {
|
||||
.package-latest-date {
|
||||
color: $muted-gray;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.package-id,
|
||||
.package-version,
|
||||
.package-date,
|
||||
.package-latest-version,
|
||||
.package-latest-date {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.package-update-available {
|
||||
.package-latest-version,
|
||||
.package-latest-date {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.package-manager-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
gap: 0.5rem;
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.package-required-by {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import { ApolloCache, DocumentNode, FetchResult } from "@apollo/client";
|
||||
import {
|
||||
ApolloCache,
|
||||
DocumentNode,
|
||||
FetchResult,
|
||||
useQuery,
|
||||
} from "@apollo/client";
|
||||
import { Modifiers } from "@apollo/client/cache";
|
||||
import {
|
||||
isField,
|
||||
|
|
@ -1961,43 +1966,6 @@ export const mutateSubmitStashBoxPerformerDraft = (
|
|||
variables: { input },
|
||||
});
|
||||
|
||||
/// Packages
|
||||
export const useInstalledScraperPackages = GQL.useInstalledScraperPackagesQuery;
|
||||
export const useInstalledScraperPackagesStatus =
|
||||
GQL.useInstalledScraperPackagesStatusQuery;
|
||||
|
||||
export const queryAvailableScraperPackages = (source: string) =>
|
||||
client.query<GQL.AvailableScraperPackagesQuery>({
|
||||
query: GQL.AvailableScraperPackagesDocument,
|
||||
variables: {
|
||||
source,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
export const useInstallScraperPackages = GQL.useInstallScraperPackagesMutation;
|
||||
export const useUpdateScraperPackages = GQL.useUpdateScraperPackagesMutation;
|
||||
export const useUninstallScraperPackages =
|
||||
GQL.useUninstallScraperPackagesMutation;
|
||||
|
||||
export const useInstalledPluginPackages = GQL.useInstalledPluginPackagesQuery;
|
||||
export const useInstalledPluginPackagesStatus =
|
||||
GQL.useInstalledPluginPackagesStatusQuery;
|
||||
|
||||
export const queryAvailablePluginPackages = (source: string) =>
|
||||
client.query<GQL.AvailablePluginPackagesQuery>({
|
||||
query: GQL.AvailablePluginPackagesDocument,
|
||||
variables: {
|
||||
source,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
export const useInstallPluginPackages = GQL.useInstallPluginPackagesMutation;
|
||||
export const useUpdatePluginPackages = GQL.useUpdatePluginPackagesMutation;
|
||||
export const useUninstallPluginPackages =
|
||||
GQL.useUninstallPluginPackagesMutation;
|
||||
|
||||
/// Configuration
|
||||
|
||||
export const useConfiguration = () => GQL.useConfigurationQuery();
|
||||
|
|
@ -2043,6 +2011,65 @@ export const useJobsSubscribe = () => GQL.useJobsSubscribeSubscription();
|
|||
|
||||
export const useLoggingSubscribe = () => GQL.useLoggingSubscribeSubscription();
|
||||
|
||||
// all scraper-related queries
|
||||
export const scraperMutationImpactedQueries = [
|
||||
GQL.ListMovieScrapersDocument,
|
||||
GQL.ListPerformerScrapersDocument,
|
||||
GQL.ListSceneScrapersDocument,
|
||||
GQL.InstalledScraperPackagesDocument,
|
||||
GQL.InstalledScraperPackagesStatusDocument,
|
||||
];
|
||||
|
||||
export const mutateReloadScrapers = () =>
|
||||
client.mutate<GQL.ReloadScrapersMutation>({
|
||||
mutation: GQL.ReloadScrapersDocument,
|
||||
update(cache, result) {
|
||||
if (!result.data?.reloadScrapers) return;
|
||||
|
||||
evictQueries(cache, scraperMutationImpactedQueries);
|
||||
},
|
||||
});
|
||||
|
||||
// all plugin-related queries
|
||||
export const pluginMutationImpactedQueries = [
|
||||
GQL.PluginsDocument,
|
||||
GQL.PluginTasksDocument,
|
||||
GQL.InstalledPluginPackagesDocument,
|
||||
GQL.InstalledPluginPackagesStatusDocument,
|
||||
];
|
||||
|
||||
export const mutateReloadPlugins = () =>
|
||||
client.mutate<GQL.ReloadPluginsMutation>({
|
||||
mutation: GQL.ReloadPluginsDocument,
|
||||
update(cache, result) {
|
||||
if (!result.data?.reloadPlugins) return;
|
||||
|
||||
evictQueries(cache, pluginMutationImpactedQueries);
|
||||
},
|
||||
});
|
||||
|
||||
type BoolMap = { [key: string]: boolean };
|
||||
|
||||
export const mutateSetPluginsEnabled = (enabledMap: BoolMap) =>
|
||||
client.mutate<GQL.SetPluginsEnabledMutation>({
|
||||
mutation: GQL.SetPluginsEnabledDocument,
|
||||
variables: { enabledMap },
|
||||
update(cache, result) {
|
||||
if (!result.data?.setPluginsEnabled) return;
|
||||
|
||||
for (const id in enabledMap) {
|
||||
cache.modify({
|
||||
id: cache.identify({ __typename: "Plugin", id }),
|
||||
fields: {
|
||||
enabled() {
|
||||
return enabledMap[id];
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function updateConfiguration(cache: ApolloCache<unknown>, result: FetchResult) {
|
||||
if (!result.data) return;
|
||||
|
||||
|
|
@ -2051,7 +2078,15 @@ function updateConfiguration(cache: ApolloCache<unknown>, result: FetchResult) {
|
|||
|
||||
export const useConfigureGeneral = () =>
|
||||
GQL.useConfigureGeneralMutation({
|
||||
update: updateConfiguration,
|
||||
update(cache, result) {
|
||||
if (!result.data?.configureGeneral) return;
|
||||
|
||||
evictQueries(cache, [
|
||||
GQL.ConfigurationDocument,
|
||||
...scraperMutationImpactedQueries,
|
||||
...pluginMutationImpactedQueries,
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
export const useConfigureInterface = () =>
|
||||
|
|
@ -2097,48 +2132,6 @@ export const useAddTempDLNAIP = () => GQL.useAddTempDlnaipMutation();
|
|||
|
||||
export const useRemoveTempDLNAIP = () => GQL.useRemoveTempDlnaipMutation();
|
||||
|
||||
export const mutateReloadScrapers = () =>
|
||||
client.mutate<GQL.ReloadScrapersMutation>({
|
||||
mutation: GQL.ReloadScrapersDocument,
|
||||
update(cache, result) {
|
||||
if (!result.data?.reloadScrapers) return;
|
||||
|
||||
evictQueries(cache, [
|
||||
GQL.ListMovieScrapersDocument,
|
||||
GQL.ListPerformerScrapersDocument,
|
||||
GQL.ListSceneScrapersDocument,
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
const pluginMutationImpactedQueries = [
|
||||
GQL.PluginsDocument,
|
||||
GQL.PluginTasksDocument,
|
||||
];
|
||||
|
||||
export const mutateReloadPlugins = () =>
|
||||
client.mutate<GQL.ReloadPluginsMutation>({
|
||||
mutation: GQL.ReloadPluginsDocument,
|
||||
update(cache, result) {
|
||||
if (!result.data?.reloadPlugins) return;
|
||||
|
||||
evictQueries(cache, pluginMutationImpactedQueries);
|
||||
},
|
||||
});
|
||||
|
||||
type BoolMap = { [key: string]: boolean };
|
||||
|
||||
export const mutateSetPluginsEnabled = (enabledMap: BoolMap) =>
|
||||
client.mutate<GQL.SetPluginsEnabledMutation>({
|
||||
mutation: GQL.SetPluginsEnabledDocument,
|
||||
variables: { enabledMap },
|
||||
update(cache, result) {
|
||||
if (!result.data?.setPluginsEnabled) return;
|
||||
|
||||
evictQueries(cache, pluginMutationImpactedQueries);
|
||||
},
|
||||
});
|
||||
|
||||
export const mutateStopJob = (jobID: string) =>
|
||||
client.mutate<GQL.StopJobMutation>({
|
||||
mutation: GQL.StopJobDocument,
|
||||
|
|
@ -2172,6 +2165,118 @@ export const mutateMigrate = (input: GQL.MigrateInput) =>
|
|||
},
|
||||
});
|
||||
|
||||
/// Packages
|
||||
|
||||
// Acts like GQL.useInstalledScraperPackagesStatusQuery if loadUpgrades is true,
|
||||
// and GQL.useInstalledScraperPackagesQuery if it is false
|
||||
export const useInstalledScraperPackages = <T extends boolean>(
|
||||
loadUpgrades: T
|
||||
) => {
|
||||
const query = loadUpgrades
|
||||
? GQL.InstalledScraperPackagesStatusDocument
|
||||
: GQL.InstalledScraperPackagesDocument;
|
||||
|
||||
type TData = T extends true
|
||||
? GQL.InstalledScraperPackagesStatusQuery
|
||||
: GQL.InstalledScraperPackagesQuery;
|
||||
type TVariables = T extends true
|
||||
? GQL.InstalledScraperPackagesStatusQueryVariables
|
||||
: GQL.InstalledScraperPackagesQueryVariables;
|
||||
|
||||
return useQuery<TData, TVariables>(query);
|
||||
};
|
||||
|
||||
export const queryAvailableScraperPackages = (source: string) =>
|
||||
client.query<GQL.AvailableScraperPackagesQuery>({
|
||||
query: GQL.AvailableScraperPackagesDocument,
|
||||
variables: {
|
||||
source,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
export const mutateInstallScraperPackages = (
|
||||
packages: GQL.PackageSpecInput[]
|
||||
) =>
|
||||
client.mutate<GQL.InstallScraperPackagesMutation>({
|
||||
mutation: GQL.InstallScraperPackagesDocument,
|
||||
variables: {
|
||||
packages,
|
||||
},
|
||||
});
|
||||
|
||||
export const mutateUpdateScraperPackages = (packages: GQL.PackageSpecInput[]) =>
|
||||
client.mutate<GQL.UpdateScraperPackagesMutation>({
|
||||
mutation: GQL.UpdateScraperPackagesDocument,
|
||||
variables: {
|
||||
packages,
|
||||
},
|
||||
});
|
||||
|
||||
export const mutateUninstallScraperPackages = (
|
||||
packages: GQL.PackageSpecInput[]
|
||||
) =>
|
||||
client.mutate<GQL.UninstallScraperPackagesMutation>({
|
||||
mutation: GQL.UninstallScraperPackagesDocument,
|
||||
variables: {
|
||||
packages,
|
||||
},
|
||||
});
|
||||
|
||||
// Acts like GQL.useInstalledPluginPackagesStatusQuery if loadUpgrades is true,
|
||||
// and GQL.useInstalledPluginPackagesQuery if it is false
|
||||
export const useInstalledPluginPackages = <T extends boolean>(
|
||||
loadUpgrades: T
|
||||
) => {
|
||||
const query = loadUpgrades
|
||||
? GQL.InstalledPluginPackagesStatusDocument
|
||||
: GQL.InstalledPluginPackagesDocument;
|
||||
|
||||
type TData = T extends true
|
||||
? GQL.InstalledPluginPackagesStatusQuery
|
||||
: GQL.InstalledPluginPackagesQuery;
|
||||
type TVariables = T extends true
|
||||
? GQL.InstalledPluginPackagesStatusQueryVariables
|
||||
: GQL.InstalledPluginPackagesQueryVariables;
|
||||
|
||||
return useQuery<TData, TVariables>(query);
|
||||
};
|
||||
|
||||
export const queryAvailablePluginPackages = (source: string) =>
|
||||
client.query<GQL.AvailablePluginPackagesQuery>({
|
||||
query: GQL.AvailablePluginPackagesDocument,
|
||||
variables: {
|
||||
source,
|
||||
},
|
||||
fetchPolicy: "network-only",
|
||||
});
|
||||
|
||||
export const mutateInstallPluginPackages = (packages: GQL.PackageSpecInput[]) =>
|
||||
client.mutate<GQL.InstallPluginPackagesMutation>({
|
||||
mutation: GQL.InstallPluginPackagesDocument,
|
||||
variables: {
|
||||
packages,
|
||||
},
|
||||
});
|
||||
|
||||
export const mutateUpdatePluginPackages = (packages: GQL.PackageSpecInput[]) =>
|
||||
client.mutate<GQL.UpdatePluginPackagesMutation>({
|
||||
mutation: GQL.UpdatePluginPackagesDocument,
|
||||
variables: {
|
||||
packages,
|
||||
},
|
||||
});
|
||||
|
||||
export const mutateUninstallPluginPackages = (
|
||||
packages: GQL.PackageSpecInput[]
|
||||
) =>
|
||||
client.mutate<GQL.UninstallPluginPackagesMutation>({
|
||||
mutation: GQL.UninstallPluginPackagesDocument,
|
||||
variables: {
|
||||
packages,
|
||||
},
|
||||
});
|
||||
|
||||
/// Tasks
|
||||
|
||||
export const mutateMetadataScan = (input: GQL.ScanMetadataInput) =>
|
||||
|
|
|
|||
|
|
@ -360,6 +360,10 @@
|
|||
"number_of_parallel_task_for_scan_generation_desc": "Set to 0 for auto-detection. Warning running more tasks than is required to achieve 100% cpu utilisation will decrease performance and potentially cause other issues.",
|
||||
"number_of_parallel_task_for_scan_generation_head": "Number of parallel task for scan/generation",
|
||||
"parallel_scan_head": "Parallel Scan/Generation",
|
||||
"plugins_path": {
|
||||
"description": "Directory location of plugin configuration files",
|
||||
"heading": "Plugins Path"
|
||||
},
|
||||
"preview_generation": "Preview Generation",
|
||||
"python_path": {
|
||||
"description": "Path to the python executable (not just the folder). Used for script scrapers and plugins. If blank, python will be resolved from the environment",
|
||||
|
|
@ -1098,6 +1102,7 @@
|
|||
"selected_only": "Selected only",
|
||||
"show_all": "Show all",
|
||||
"uninstall": "Uninstall",
|
||||
"unknown": "<unknown>",
|
||||
"update": "Update",
|
||||
"version": "Version"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue