spinuptui 0.26.1 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -1
- package/README.md +9 -5
- package/package.json +1 -1
- package/src/api/client.ts +38 -2
- package/src/config.ts +232 -8
- package/src/index.tsx +25 -5
- package/src/lib/dnsCache.ts +9 -6
- package/src/lib/keychain.ts +11 -5
- package/src/lib/probe.ts +16 -0
- package/src/lib/providersCache.ts +9 -6
- package/src/lib/stackCache.ts +9 -6
- package/src/lib/wpCli.ts +2 -2
- package/src/lib/wpCore.ts +273 -0
- package/src/ui/App.tsx +18 -2
- package/src/ui/Explain.tsx +5 -4
- package/src/ui/Header.tsx +6 -2
- package/src/ui/Help.tsx +2 -1
- package/src/ui/ToastStack.tsx +2 -2
- package/src/ui/accounts.tsx +21 -0
- package/src/ui/components.tsx +1 -1
- package/src/ui/store.tsx +89 -21
- package/src/ui/toast.ts +16 -10
- package/src/ui/views/Accounts.tsx +370 -0
- package/src/ui/views/Browser.tsx +2 -0
- package/src/ui/views/BulkWpUpdate.tsx +396 -0
- package/src/ui/views/PhpUpgrade.tsx +5 -2
- package/src/ui/views/Search.tsx +2 -0
- package/src/ui/views/SiteUpdate.tsx +124 -0
- package/src/ui/views/Stacks.tsx +113 -15
- package/src/ui/views/WpCoreUpdate.tsx +322 -0
- package/src/ui/wpCoreJobs.tsx +253 -0
package/src/ui/store.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import { SpinupWPClient, ApiError, type ServerService, type SpinupWPClientLike }
|
|
|
11
11
|
import { isDevMode } from "../dev/devMode.ts"
|
|
12
12
|
import { createMockClient } from "../dev/mockClient.ts"
|
|
13
13
|
import type { Server, Site, Event, ProviderMetadata, CreateServerPayload, CreateSitePayload, AdditionalDomain } from "../api/types.ts"
|
|
14
|
-
import { loadConfig, saveConfig, type ServerProviderRef, type KumaMonitorRef, type UptimeKumaConn } from "../config.ts"
|
|
14
|
+
import { loadConfig, saveConfig, type StoredConfig, type ServerProviderRef, type KumaMonitorRef, type UptimeKumaConn } from "../config.ts"
|
|
15
15
|
import { saveJob, removeJob } from "../lib/jobs.ts"
|
|
16
16
|
import { APP_VERSION } from "../version.ts"
|
|
17
17
|
import { cachedUpdateInfo, refreshUpdateInfo, type UpdateInfo } from "../lib/appUpdate.ts"
|
|
@@ -20,7 +20,7 @@ import { resolveLocalLink, expandPath, normalizeLink, type LocalLink } from "../
|
|
|
20
20
|
import { cloneStackFor, type Stack } from "../lib/stack.ts"
|
|
21
21
|
import { openTerminalAt, openUrl, openSshSession } from "../lib/open.ts"
|
|
22
22
|
import { gitDrift, type Drift } from "../lib/gitStatus.ts"
|
|
23
|
-
import { probeSite } from "../lib/probe.ts"
|
|
23
|
+
import { probeSite, withWpVersion } from "../lib/probe.ts"
|
|
24
24
|
import { resolveZone, normalizeDomain, candidateHostnames, type ZoneHost } from "../lib/dns.ts"
|
|
25
25
|
import { queryAuthoritative } from "../lib/dnsQuery.ts"
|
|
26
26
|
import { DnsCache, type CachedDns } from "../lib/dnsCache.ts"
|
|
@@ -53,9 +53,10 @@ import { keychainAvailable, setSudoPassword, getSudoPassword, deleteSudoPassword
|
|
|
53
53
|
import { StackCache, siteSignature, type CachedProbe } from "../lib/stackCache.ts"
|
|
54
54
|
import { resolvePhpEolDates, refreshPhpEolDates, isPhpEol as isPhpEolWith, offeredPhpVersions as offeredPhpVersionsWith, type PhpEolDates } from "../lib/phpEol.ts"
|
|
55
55
|
import { resolveUbuntuEolDates, refreshUbuntuEolDates, isUbuntuEol as isUbuntuEolWith, type UbuntuEolDates } from "../lib/ubuntuEol.ts"
|
|
56
|
-
import {
|
|
57
|
-
import {
|
|
58
|
-
import {
|
|
56
|
+
import { wpCoreBusy } from "./wpCoreJobs.tsx"
|
|
57
|
+
import { planDbBackup, runDbBackup, isDbBackupInFlight, type DbBackupProgress, type PlanResult } from "../lib/dbBackup.ts"
|
|
58
|
+
import { planDbSync, runDbSync, isDbSyncInFlight, type DbSyncProgress, type SyncPlanResult } from "../lib/dbSync.ts"
|
|
59
|
+
import { planLocalSetup, runLocalSetup, isLocalSetupInFlight, type LocalSetupProgress, type LocalSetupPlanResult } from "../lib/localSetup.ts"
|
|
59
60
|
import { planMediaFallback, type MediaFallbackResult } from "../lib/mediaFallback.ts"
|
|
60
61
|
|
|
61
62
|
export type Route = "dashboard" | "servers" | "stacks" | "search" | "events"
|
|
@@ -484,6 +485,11 @@ interface StoreValue extends DataState {
|
|
|
484
485
|
// The site whose PHP-upgrade overlay is open, or null. Set by site views.
|
|
485
486
|
phpUpgradeSite: Site | null
|
|
486
487
|
setPhpUpgradeSite: (s: Site | null) => void
|
|
488
|
+
// The Stacks version group whose bulk WordPress-update overlay is open, or null.
|
|
489
|
+
// The sites are a snapshot taken when it opened (the group itself reshuffles as
|
|
490
|
+
// updates land and versions change).
|
|
491
|
+
bulkWpGroup: { title: string; sites: Site[] } | null
|
|
492
|
+
setBulkWpGroup: (g: { title: string; sites: Site[] } | null) => void
|
|
487
493
|
// In-flight (and just-failed) PHP upgrades, keyed by site id. Tracked in the
|
|
488
494
|
// store — not the overlay — so progress survives closing the modal; site rows
|
|
489
495
|
// and detail panels read this to show a spinner/marker.
|
|
@@ -749,6 +755,18 @@ interface StoreValue extends DataState {
|
|
|
749
755
|
enableLocalSync: () => void
|
|
750
756
|
// SpinupWP account slug (from env/config) for building web deep links.
|
|
751
757
|
accountSlug: string | null
|
|
758
|
+
// The account (profile) this store is mounted for — see config.ts "Accounts".
|
|
759
|
+
// Switching remounts the whole store, so these never change under a view.
|
|
760
|
+
profileId: string
|
|
761
|
+
profileLabel: string
|
|
762
|
+
setProfileLabel: (label: string) => void // after a rename in the Accounts overlay
|
|
763
|
+
// The token came from SPINUPWP_ACCESS_TOKEN, which pins the account: switching
|
|
764
|
+
// would change nothing until it's unset.
|
|
765
|
+
tokenFromEnv: boolean
|
|
766
|
+
// What's still running that an account switch would orphan, in words — or
|
|
767
|
+
// null when it's safe to switch. Only work that writes (to SpinupWP, a server,
|
|
768
|
+
// DNS, or a local database) counts; reads and probes are simply dropped.
|
|
769
|
+
switchBlocker: () => string | null
|
|
752
770
|
sitesForServer: (serverId: number) => Site[]
|
|
753
771
|
serverById: (id: number | null | undefined) => Server | undefined
|
|
754
772
|
// Tier-2 stack probes (on-demand SSH), hydrated from disk at startup.
|
|
@@ -757,6 +775,9 @@ interface StoreValue extends DataState {
|
|
|
757
775
|
probeErrors: Map<number, string> // last error per site id
|
|
758
776
|
// Probe a single site over SSH (fire-and-forget); write-through to the cache.
|
|
759
777
|
runProbe: (site: Site) => void
|
|
778
|
+
// Fold a WP core version read elsewhere (the `u` → WordPress check/update) into
|
|
779
|
+
// the cached probe, so rows showing "WordPress x.y.z" don't lag behind it.
|
|
780
|
+
noteWpVersion: (site: Site, version: string) => void
|
|
760
781
|
// Probe many sites with a bounded concurrency pool (skips in-flight sites).
|
|
761
782
|
runProbeMany: (sites: Site[]) => void
|
|
762
783
|
// Whether a cached probe for this site is stale (site shape changed since).
|
|
@@ -887,6 +908,9 @@ export function useStore(): StoreValue {
|
|
|
887
908
|
|
|
888
909
|
export function StoreProvider({ children }: { children: ReactNode }) {
|
|
889
910
|
const cfgRef = useRef(loadConfig())
|
|
911
|
+
// Every config write from this store targets the account it was mounted for —
|
|
912
|
+
// a late write from an async flow can't land in an account switched to since.
|
|
913
|
+
const saveProfileConfig = (partial: StoredConfig) => saveConfig(partial, cfgRef.current.profileId)
|
|
890
914
|
const clientRef = useRef<SpinupWPClientLike | null>(null)
|
|
891
915
|
if (!clientRef.current) {
|
|
892
916
|
clientRef.current = isDevMode() ? createMockClient() : new SpinupWPClient(cfgRef.current)
|
|
@@ -919,7 +943,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
919
943
|
// Fresh install, or upgrading from a pre-this-feature version — nothing
|
|
920
944
|
// meaningful to announce. Seed silently so future bumps trigger correctly.
|
|
921
945
|
cfgRef.current.lastSeenVersion = APP_VERSION
|
|
922
|
-
void
|
|
946
|
+
void saveProfileConfig({ lastSeenVersion: APP_VERSION })
|
|
923
947
|
return
|
|
924
948
|
}
|
|
925
949
|
void fetchReleaseNotes(APP_VERSION).then((notes) => {
|
|
@@ -931,7 +955,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
931
955
|
const dismissReleaseNotes = useCallback(() => {
|
|
932
956
|
setReleaseNotesInfo(null)
|
|
933
957
|
cfgRef.current.lastSeenVersion = APP_VERSION
|
|
934
|
-
void
|
|
958
|
+
void saveProfileConfig({ lastSeenVersion: APP_VERSION })
|
|
935
959
|
}, [])
|
|
936
960
|
// On-demand replay (Help overlay's `n`) — bypasses the "seen" gate entirely,
|
|
937
961
|
// since viewing the current version's notes again is always valid regardless
|
|
@@ -943,12 +967,14 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
943
967
|
if (notes) setReleaseNotesInfo(notes)
|
|
944
968
|
})
|
|
945
969
|
}, [])
|
|
970
|
+
const [profileLabel, setProfileLabel] = useState(cfgRef.current.profileLabel)
|
|
946
971
|
const [route, setRoute] = useState<Route>("dashboard")
|
|
947
972
|
const [inputMode, setInputMode] = useState(false)
|
|
948
973
|
const [overlayOpen, setOverlayOpen] = useState(false)
|
|
949
974
|
const [healthServer, setHealthServer] = useState<Server | null>(null)
|
|
950
975
|
const [wpInventorySite, setWpInventorySite] = useState<Site | null>(null)
|
|
951
976
|
const [phpUpgradeSite, setPhpUpgradeSite] = useState<Site | null>(null)
|
|
977
|
+
const [bulkWpGroup, setBulkWpGroup] = useState<{ title: string; sites: Site[] } | null>(null)
|
|
952
978
|
const [phpUpgrades, setPhpUpgrades] = useState<Map<number, PhpUpgradeProgress>>(new Map())
|
|
953
979
|
const [httpsToggleSite, setHttpsToggleSite] = useState<Site | null>(null)
|
|
954
980
|
const [httpsToggles, setHttpsToggles] = useState<Map<number, HttpsToggleProgress>>(new Map())
|
|
@@ -1147,6 +1173,17 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1147
1173
|
|
|
1148
1174
|
// Probe one site and reconcile state + cache. Concurrency-safe (all state
|
|
1149
1175
|
// updates are functional), so the batch runner can pool several at once.
|
|
1176
|
+
const noteWpVersion = useCallback((site: Site, version: string) => {
|
|
1177
|
+
const cache = cacheRef.current!
|
|
1178
|
+
const entry = cache.get(site.id)
|
|
1179
|
+
// Only refresh an existing WP-family probe: with no probe we don't know the
|
|
1180
|
+
// stack (Standard vs Bedrock vs Radicle), and that's the probe's job.
|
|
1181
|
+
if (!entry || entry.result.version === version) return
|
|
1182
|
+
const next = withWpVersion(entry.result, version)
|
|
1183
|
+
if (!next) return
|
|
1184
|
+
void cache.set(site.id, next, siteSignature(site)).then(() => setProbes(cache.snapshot()))
|
|
1185
|
+
}, [])
|
|
1186
|
+
|
|
1150
1187
|
const probeOne = useCallback(
|
|
1151
1188
|
async (site: Site) => {
|
|
1152
1189
|
const cache = cacheRef.current!
|
|
@@ -1620,7 +1657,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1620
1657
|
const next = { ...prev, [providerKey]: { ...(prev[providerKey] ?? {}), id } }
|
|
1621
1658
|
cfgRef.current.serverProviders = next
|
|
1622
1659
|
setServerProviders(next)
|
|
1623
|
-
void
|
|
1660
|
+
void saveProfileConfig({ serverProviders: next })
|
|
1624
1661
|
}, [])
|
|
1625
1662
|
|
|
1626
1663
|
// Poll a server-create event to completion, mirroring its status into the job
|
|
@@ -1714,7 +1751,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1714
1751
|
const record: Record<string, LocalLink> = {}
|
|
1715
1752
|
for (const [id, link] of next) record[String(id)] = link
|
|
1716
1753
|
cfgRef.current.localSites = record
|
|
1717
|
-
void
|
|
1754
|
+
void saveProfileConfig({ localSites: record })
|
|
1718
1755
|
}, [])
|
|
1719
1756
|
|
|
1720
1757
|
const linkSite = useCallback(
|
|
@@ -1746,14 +1783,14 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1746
1783
|
if (prev.includes(trimmed)) return prev
|
|
1747
1784
|
const next = [...prev, trimmed]
|
|
1748
1785
|
cfgRef.current.localRoots = next
|
|
1749
|
-
void
|
|
1786
|
+
void saveProfileConfig({ localRoots: next })
|
|
1750
1787
|
return next
|
|
1751
1788
|
})
|
|
1752
1789
|
}, [])
|
|
1753
1790
|
|
|
1754
1791
|
const enableLocalSync = useCallback(() => {
|
|
1755
1792
|
cfgRef.current.localSync = true
|
|
1756
|
-
void
|
|
1793
|
+
void saveProfileConfig({ localSync: true })
|
|
1757
1794
|
setLocalSyncState(true)
|
|
1758
1795
|
}, [])
|
|
1759
1796
|
|
|
@@ -1803,7 +1840,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1803
1840
|
const record: Record<string, { user: string; keychain?: boolean }> = {}
|
|
1804
1841
|
for (const [id, u] of users) record[String(id)] = saved.has(id) ? { user: u, keychain: true } : { user: u }
|
|
1805
1842
|
cfgRef.current.sudoUsers = record
|
|
1806
|
-
void
|
|
1843
|
+
void saveProfileConfig({ sudoUsers: record })
|
|
1807
1844
|
}, [])
|
|
1808
1845
|
const isSudoConnected = useCallback((serverId: number) => sudoConnected.has(serverId), [sudoConnected])
|
|
1809
1846
|
const sudoSavedFor = useCallback((serverId: number) => sudoSaved.has(serverId), [sudoSaved])
|
|
@@ -1900,7 +1937,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1900
1937
|
const setPreferredGrantKeys = useCallback((ids: string[]) => {
|
|
1901
1938
|
cfgRef.current.preferredGrantKeys = ids
|
|
1902
1939
|
setPreferredGrantKeysState(ids)
|
|
1903
|
-
void
|
|
1940
|
+
void saveProfileConfig({ preferredGrantKeys: ids })
|
|
1904
1941
|
}, [])
|
|
1905
1942
|
|
|
1906
1943
|
// Persist the granted-keys map (only non-empty entries) to config, write-through.
|
|
@@ -1908,7 +1945,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1908
1945
|
const rec: Record<string, string[]> = {}
|
|
1909
1946
|
for (const [id, set] of next) if (set.size) rec[String(id)] = [...set]
|
|
1910
1947
|
cfgRef.current.grantedKeys = rec
|
|
1911
|
-
void
|
|
1948
|
+
void saveProfileConfig({ grantedKeys: rec })
|
|
1912
1949
|
}, [])
|
|
1913
1950
|
|
|
1914
1951
|
// Record / forget which keys (by body) Spinup has granted on a site.
|
|
@@ -1950,7 +1987,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
1950
1987
|
else next.delete(apex)
|
|
1951
1988
|
const rec = Object.fromEntries(next)
|
|
1952
1989
|
cfgRef.current.zoneAccessNotes = rec
|
|
1953
|
-
void
|
|
1990
|
+
void saveProfileConfig({ zoneAccessNotes: rec })
|
|
1954
1991
|
return next
|
|
1955
1992
|
})
|
|
1956
1993
|
}, [])
|
|
@@ -2427,7 +2464,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
2427
2464
|
for (const p of ALL_PROVIDERS) {
|
|
2428
2465
|
providers[p] = next[p].filter((c) => !c.env).map((c) => ({ id: c.id, label: c.label, creds: c.creds }))
|
|
2429
2466
|
}
|
|
2430
|
-
void
|
|
2467
|
+
void saveProfileConfig({ providers })
|
|
2431
2468
|
}, [])
|
|
2432
2469
|
|
|
2433
2470
|
// Verify a connection and write the result through to the cache.
|
|
@@ -2662,7 +2699,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
2662
2699
|
if (keys[domain] !== key) {
|
|
2663
2700
|
keys[domain] = key
|
|
2664
2701
|
cfgRef.current.vanityHealthKeys = keys
|
|
2665
|
-
void
|
|
2702
|
+
void saveProfileConfig({ vanityHealthKeys: keys })
|
|
2666
2703
|
}
|
|
2667
2704
|
return key
|
|
2668
2705
|
}, [])
|
|
@@ -2676,7 +2713,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
2676
2713
|
const newKey = crypto.randomUUID().replace(/-/g, "")
|
|
2677
2714
|
keys[domain] = newKey
|
|
2678
2715
|
cfgRef.current.vanityHealthKeys = keys
|
|
2679
|
-
void
|
|
2716
|
+
void saveProfileConfig({ vanityHealthKeys: keys })
|
|
2680
2717
|
return { oldKey, newKey }
|
|
2681
2718
|
}, [])
|
|
2682
2719
|
|
|
@@ -2685,13 +2722,13 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
2685
2722
|
const persistKumaAuth = useCallback((conn: UptimeKumaConn, jwt: string) => {
|
|
2686
2723
|
if (conn.env || jwt === conn.jwt) return
|
|
2687
2724
|
cfgRef.current.uptimeKuma = { ...conn, jwt }
|
|
2688
|
-
void
|
|
2725
|
+
void saveProfileConfig({ uptimeKuma: { ...conn, jwt } })
|
|
2689
2726
|
}, [])
|
|
2690
2727
|
|
|
2691
2728
|
const persistKumaMonitors = useCallback((domain: string, ref: KumaMonitorRef) => {
|
|
2692
2729
|
const map = { ...cfgRef.current.kumaMonitors, [domain]: ref }
|
|
2693
2730
|
cfgRef.current.kumaMonitors = map
|
|
2694
|
-
void
|
|
2731
|
+
void saveProfileConfig({ kumaMonitors: map })
|
|
2695
2732
|
}, [])
|
|
2696
2733
|
|
|
2697
2734
|
// The step machine. Each step performs its work then hands off to the next; it
|
|
@@ -3107,7 +3144,7 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
3107
3144
|
if (!login.ok) return { ok: false, error: login.error, tokenRequired: login.tokenRequired }
|
|
3108
3145
|
const stored = { url, username: conn.username, password: conn.password, jwt: login.jwt }
|
|
3109
3146
|
cfgRef.current.uptimeKuma = stored
|
|
3110
|
-
void
|
|
3147
|
+
void saveProfileConfig({ uptimeKuma: stored })
|
|
3111
3148
|
setKumaEpoch((e) => e + 1) // start the status poll loop now
|
|
3112
3149
|
await kuma.waitForVersion() // `info` can land after the login ack
|
|
3113
3150
|
return { ok: true, version: kuma.version }
|
|
@@ -4225,6 +4262,29 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
4225
4262
|
}
|
|
4226
4263
|
}, [trackServerEvent, trackPhpUpgradeEvent, trackHttpsToggleEvent, driveVanity])
|
|
4227
4264
|
|
|
4265
|
+
const switchBlocker = (): string | null => {
|
|
4266
|
+
const count = <T,>(m: Map<unknown, T>, busy: (p: T) => boolean) => [...m.values()].filter(busy).length
|
|
4267
|
+
const running: [number, string][] = [
|
|
4268
|
+
[count(phpUpgrades, isUpgradeInFlight), "PHP upgrade"],
|
|
4269
|
+
[count(httpsToggles, isHttpsToggleInFlight), "HTTPS change"],
|
|
4270
|
+
[count(purgeCacheProgress, isPurgeCacheInFlight), "cache purge"],
|
|
4271
|
+
[count(serverOps, isServerOpInFlight), "server reboot/restart"],
|
|
4272
|
+
[count(keyGrants, isKeyGrantInFlight), "SSH key grant"],
|
|
4273
|
+
[count(dbBackups, isDbBackupInFlight), "database backup"],
|
|
4274
|
+
[count(dbSyncs, isDbSyncInFlight), "database sync"],
|
|
4275
|
+
[count(localSetups, isLocalSetupInFlight), "local copy setup"],
|
|
4276
|
+
[count(recordWrites, isRecordWriteInFlight), "DNS change"],
|
|
4277
|
+
[count(kumaOps, (o) => o.status === "running"), "monitoring change"],
|
|
4278
|
+
[isNewServerInFlight(newServerJob) ? 1 : 0, "server build"],
|
|
4279
|
+
[isVanityInFlight(vanityJob) ? 1 : 0, "vanity-site build"],
|
|
4280
|
+
// A clone still being planned hasn't touched anything yet — only a launched one counts.
|
|
4281
|
+
[isCloneInFlight(cloneJob) && cloneJob!.step !== "plan" && cloneJob!.step !== "server" ? 1 : 0, "server clone"],
|
|
4282
|
+
[wpCoreBusy() ? 1 : 0, "WordPress update"],
|
|
4283
|
+
]
|
|
4284
|
+
const busy = running.filter(([n]) => n > 0).map(([n, what]) => (n > 1 ? `${n} ${what}s` : `a ${what}`))
|
|
4285
|
+
return busy.length ? `Still running: ${busy.join(", ")}. Switch accounts once ${busy.length > 1 ? "they finish" : "it finishes"}.` : null
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4228
4288
|
const value: StoreValue = {
|
|
4229
4289
|
servers,
|
|
4230
4290
|
sites,
|
|
@@ -4249,6 +4309,8 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
4249
4309
|
setWpInventorySite,
|
|
4250
4310
|
phpUpgradeSite,
|
|
4251
4311
|
setPhpUpgradeSite,
|
|
4312
|
+
bulkWpGroup,
|
|
4313
|
+
setBulkWpGroup,
|
|
4252
4314
|
phpUpgrades,
|
|
4253
4315
|
startPhpUpgrade,
|
|
4254
4316
|
clearPhpUpgrade,
|
|
@@ -4415,12 +4477,18 @@ export function StoreProvider({ children }: { children: ReactNode }) {
|
|
|
4415
4477
|
setEnableLocalSyncSite,
|
|
4416
4478
|
enableLocalSync,
|
|
4417
4479
|
accountSlug: cfgRef.current.accountSlug,
|
|
4480
|
+
profileId: cfgRef.current.profileId,
|
|
4481
|
+
profileLabel,
|
|
4482
|
+
setProfileLabel,
|
|
4483
|
+
tokenFromEnv: cfgRef.current.tokenSource === "env",
|
|
4484
|
+
switchBlocker,
|
|
4418
4485
|
sitesForServer,
|
|
4419
4486
|
serverById,
|
|
4420
4487
|
probes,
|
|
4421
4488
|
probingIds,
|
|
4422
4489
|
probeErrors,
|
|
4423
4490
|
runProbe,
|
|
4491
|
+
noteWpVersion,
|
|
4424
4492
|
runProbeMany,
|
|
4425
4493
|
isProbeStale,
|
|
4426
4494
|
dnsZones,
|
package/src/ui/toast.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Replaces the @opentui-ui/toast package, which is stuck on a peer-dependency
|
|
4
4
|
// range from opentui's 0.1.x days and breaks against the @opentui/core 0.4.x
|
|
5
5
|
// line we're on (see CHANGELOG v0.24.2/v0.24.3). Every call site in this app
|
|
6
|
-
// only ever fires a single-line success message, so a small first-party
|
|
6
|
+
// only ever fires a single-line success or error message, so a small first-party
|
|
7
7
|
// implementation is simpler — and immune to upstream drift — than depending
|
|
8
8
|
// on a package built for a much larger feature set we don't use.
|
|
9
9
|
//
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
export interface ToastItem {
|
|
16
16
|
id: number
|
|
17
17
|
message: string
|
|
18
|
+
kind: "success" | "error"
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
const TOAST_DURATION_MS = 4000
|
|
@@ -36,14 +37,19 @@ export function getToasts(): ToastItem[] {
|
|
|
36
37
|
return toasts
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
function push(message: string, kind: ToastItem["kind"]) {
|
|
41
|
+
const id = nextId++
|
|
42
|
+
toasts = [...toasts, { id, message, kind }]
|
|
43
|
+
notify()
|
|
44
|
+
setTimeout(() => {
|
|
45
|
+
toasts = toasts.filter((t) => t.id !== id)
|
|
43
46
|
notify()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
}, TOAST_DURATION_MS)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const toast = {
|
|
51
|
+
success: (message: string) => push(message, "success"),
|
|
52
|
+
// For a background job that failed after its overlay was closed — points the
|
|
53
|
+
// user back at where the details are.
|
|
54
|
+
error: (message: string) => push(message, "error"),
|
|
49
55
|
}
|