mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
RR Scaffolding Work (#18658)
* Init demo world map for read replicas * Update package.json * Scaffold and mock data for flow view of project settings infrastructure configuration * Shift map stuff and install deck.gl to experiment * Flesh out map view a bit more * Scaffold deploy new replica panel * Add restart replica option * Add animations, shift node generation into utils, attempt to show region nodes * Experiments * General improvement to chart and map UI * Improve some copy writing for restart and drop replica modals * Simplify loading, error, success states in DatabaseSettings * Scaffold DB selection in connection string section * Link replica node to connection string * Scaffold database selection for SQL editor * Make DatabaseSelector into a component * Add database selector to infra activity * Update * Hide read replicas UI behind a feature flag * Update flags * Update package-lock --------- Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com>
This commit is contained in:
1 parent
81e146f26b
commit
3620ca8ebe
23 files changed
+13043
-289
No files matched your search
@@ -3,8 +3,11 @@ import { IS_PLATFORM } from 'lib/constants'
|
||||
import { detectOS } from 'lib/helpers'
|
||||
import { Button, IconAlignLeft, IconCommand, IconCornerDownLeft } from 'ui'
|
||||
|
||||
import DatabaseSelector from 'components/ui/DatabaseSelector'
|
||||
import { useState } from 'react'
|
||||
import FavoriteButton from './FavoriteButton'
|
||||
import SavingIndicator from './SavingIndicator'
|
||||
import { useFlag } from 'hooks'
|
||||
import SizeToggleButton from './SizeToggleButton'
|
||||
|
||||
export type UtilityActionsProps = {
|
||||
@@ -25,13 +28,20 @@ const UtilityActions = ({
|
||||
executeQuery,
|
||||
}: UtilityActionsProps) => {
|
||||
const os = detectOS()
|
||||
const readReplicasEnabled = useFlag('readReplicas')
|
||||
const [selectedDatabaseId, setSelectedDatabaseId] = useState<string>('1')
|
||||
|
||||
return (
|
||||
<>
|
||||
<SavingIndicator id={id} />
|
||||
|
||||
{IS_PLATFORM && <FavoriteButton id={id} />}
|
||||
<SizeToggleButton id={id} />
|
||||
|
||||
{/* [Joshen] Am opting to remove this - i don't think its useful? */}
|
||||
{/* [Joshen] Keeping in mind to not sprawl controls everywhere */}
|
||||
{/* [Joshen] There's eventually gonna be user impersonation here as well so let's see */}
|
||||
{/* <SizeToggleButton id={id} /> */}
|
||||
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
@@ -54,26 +64,35 @@ const UtilityActions = ({
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
<Button
|
||||
onClick={() => executeQuery()}
|
||||
disabled={isDisabled || isExecuting}
|
||||
loading={isExecuting}
|
||||
type="default"
|
||||
size="tiny"
|
||||
className="mx-2"
|
||||
iconRight={
|
||||
<div className="flex items-center space-x-1">
|
||||
{os === 'macos' ? (
|
||||
<IconCommand size={10} strokeWidth={1.5} />
|
||||
) : (
|
||||
<p className="text-xs text-foreground-light">CTRL</p>
|
||||
)}
|
||||
<IconCornerDownLeft size={10} strokeWidth={1.5} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{hasSelection ? 'Run selected' : 'Run'}
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-between gap-x-2 mx-2">
|
||||
{readReplicasEnabled && (
|
||||
<DatabaseSelector
|
||||
selectedDatabaseId={selectedDatabaseId}
|
||||
onChangeDatabaseId={setSelectedDatabaseId}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => executeQuery()}
|
||||
disabled={isDisabled || isExecuting}
|
||||
loading={isExecuting}
|
||||
type="default"
|
||||
size="tiny"
|
||||
iconRight={
|
||||
<div className="flex items-center space-x-1">
|
||||
{os === 'macos' ? (
|
||||
<IconCommand size={10} strokeWidth={1.5} />
|
||||
) : (
|
||||
<p className="text-xs text-foreground-light">CTRL</p>
|
||||
)}
|
||||
<IconCornerDownLeft size={10} strokeWidth={1.5} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{hasSelection ? 'Run selected' : 'Run'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
+220
-251
@@ -1,7 +1,7 @@
|
||||
import { useParams, useTelemetryProps } from 'common'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
@@ -13,26 +13,39 @@ import {
|
||||
Tabs,
|
||||
} from 'ui'
|
||||
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import DatabaseSelector from 'components/ui/DatabaseSelector'
|
||||
import Panel from 'components/ui/Panel'
|
||||
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
|
||||
import { useProjectSettingsQuery } from 'data/config/project-settings-query'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { useFlag, useSelectedOrganization } from 'hooks'
|
||||
import { pluckObjectFields } from 'lib/helpers'
|
||||
import Telemetry from 'lib/telemetry'
|
||||
import { MOCK_DATABASES } from '../../Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
|
||||
import ConfirmDisableReadOnlyModeModal from './ConfirmDisableReadOnlyModal'
|
||||
import ResetDbPassword from './ResetDbPassword'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
|
||||
const DatabaseSettings = () => {
|
||||
const router = useRouter()
|
||||
const { ref: projectRef } = useParams()
|
||||
const { ref: projectRef, connectionString } = useParams()
|
||||
const telemetryProps = useTelemetryProps()
|
||||
const organization = useSelectedOrganization()
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
|
||||
const readReplicasEnabled = useFlag('readReplicas')
|
||||
const connectionStringsRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [selectedDatabaseId, setSelectedDatabaseId] = useState<string>('1')
|
||||
const [showConfirmationModal, setShowConfirmationModal] = useState(false)
|
||||
|
||||
const { data, isLoading, isError } = useProjectSettingsQuery({ projectRef })
|
||||
// [Joshen] Read replicas mock UI stuff
|
||||
const [open, setOpen] = useState(false)
|
||||
const databases = MOCK_DATABASES
|
||||
const selectedDatabase = databases.find((db) => db.id.toString() === selectedDatabaseId)
|
||||
|
||||
const { data, error, isLoading, isError, isSuccess } = useProjectSettingsQuery({ projectRef })
|
||||
const { data: subscription } = useOrgSubscriptionQuery({ orgSlug: organization?.slug })
|
||||
const { data: resourceWarnings } = useResourceWarningsQuery()
|
||||
|
||||
@@ -40,97 +53,12 @@ const DatabaseSettings = () => {
|
||||
(resourceWarnings ?? [])?.find((warning) => warning.project === projectRef)
|
||||
?.is_readonly_mode_enabled ?? false
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="mx-auto p-6 text-center sm:w-full md:w-3/4">
|
||||
<p className="text-foreground-light">Error loading database settings</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
<section className="space-y-6">
|
||||
<h3 className="text-foreground mb-2 text-xl">Database Settings</h3>
|
||||
<Panel
|
||||
title={
|
||||
<h5 key="panel-title" className="mb-0">
|
||||
Connection info
|
||||
</h5>
|
||||
}
|
||||
className="!m-0"
|
||||
>
|
||||
<Panel.Content className="space-y-8">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="grid gap-2 items-center md:grid md:grid-cols-12 md:gap-x-4 w-full"
|
||||
>
|
||||
<ShimmeringLoader className="h-4 w-1/3 col-span-4" delayIndex={i} />
|
||||
<ShimmeringLoader className="h-8 w-full col-span-8" delayIndex={i} />
|
||||
</div>
|
||||
))}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
|
||||
<ResetDbPassword disabled />
|
||||
|
||||
<section className="space-y-6">
|
||||
<Panel
|
||||
title={
|
||||
<h5 key="panel-title" className="mb-0" id="connection-string">
|
||||
Connection string
|
||||
</h5>
|
||||
}
|
||||
className="!m-0"
|
||||
>
|
||||
<Panel.Content>
|
||||
<Tabs type="underlined" size="small">
|
||||
<Tabs.Panel id="psql" label="PSQL">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="uri" label="URI">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="golang" label="Golang">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="jdbc" label="JDBC">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="dotnet" label=".NET">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="nodejs" label="Nodejs">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="php" label="PHP">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="python" label="Python">
|
||||
<ShimmeringLoader className="h-8 w-full" />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { project } = data
|
||||
|
||||
const { project } = data ?? {}
|
||||
const DB_FIELDS = ['db_host', 'db_name', 'db_port', 'db_user', 'inserted_at']
|
||||
const connectionInfo = pluckObjectFields(project, DB_FIELDS)
|
||||
const connectionInfo =
|
||||
project !== undefined
|
||||
? pluckObjectFields(project, DB_FIELDS)
|
||||
: { db_user: '', db_host: '', db_port: '', db_name: '' }
|
||||
|
||||
const handleCopy = (labelValue?: string) =>
|
||||
Telemetry.sendEvent(
|
||||
@@ -155,6 +83,13 @@ const DatabaseSettings = () => {
|
||||
`${connectionInfo.db_port.toString()} -d ${connectionInfo.db_name} ` +
|
||||
`-U ${connectionInfo.db_user}`
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionString !== undefined && connectionStringsRef.current !== undefined) {
|
||||
setSelectedDatabaseId(connectionString)
|
||||
connectionStringsRef.current?.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
}
|
||||
}, [connectionString])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-10">
|
||||
@@ -224,189 +159,223 @@ const DatabaseSettings = () => {
|
||||
className="!m-0"
|
||||
>
|
||||
<Panel.Content className="space-y-6">
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_host}
|
||||
label="Host"
|
||||
onCopy={() => {
|
||||
handleCopy('Host')
|
||||
}}
|
||||
/>
|
||||
{isLoading &&
|
||||
Array.from({ length: 5 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="grid gap-2 items-center md:grid md:grid-cols-12 md:gap-x-4 w-full"
|
||||
>
|
||||
<ShimmeringLoader className="h-4 w-1/3 col-span-4" delayIndex={i} />
|
||||
<ShimmeringLoader className="h-8 w-full col-span-8" delayIndex={i} />
|
||||
</div>
|
||||
))}
|
||||
{isError && (
|
||||
<AlertError error={error} subject="Failed to retrieve database settings" />
|
||||
)}
|
||||
{isSuccess && (
|
||||
<>
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_host}
|
||||
label="Host"
|
||||
onCopy={() => {
|
||||
handleCopy('Host')
|
||||
}}
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_name}
|
||||
label="Database name"
|
||||
/>
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_name}
|
||||
label="Database name"
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_port.toString()}
|
||||
label="Port"
|
||||
/>
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_port.toString()}
|
||||
label="Port"
|
||||
/>
|
||||
|
||||
<Input
|
||||
layout="horizontal"
|
||||
className="input-mono table-input-cell text-base"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_user}
|
||||
label="User"
|
||||
/>
|
||||
<Input
|
||||
layout="horizontal"
|
||||
className="input-mono table-input-cell text-base"
|
||||
readOnly
|
||||
copy
|
||||
disabled
|
||||
value={connectionInfo.db_user}
|
||||
label="User"
|
||||
/>
|
||||
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
disabled
|
||||
readOnly
|
||||
value={'[The password you provided when you created this project]'}
|
||||
label="Password"
|
||||
/>
|
||||
<Input
|
||||
className="input-mono"
|
||||
layout="horizontal"
|
||||
disabled
|
||||
readOnly
|
||||
value={'[The password you provided when you created this project]'}
|
||||
label="Password"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
|
||||
<ResetDbPassword />
|
||||
<ResetDbPassword disabled={isLoading || isError} />
|
||||
|
||||
<section className="space-y-6">
|
||||
<Panel
|
||||
title={
|
||||
<h5 key="panel-title" className="mb-0">
|
||||
Connection string
|
||||
</h5>
|
||||
<div ref={connectionStringsRef} className="w-full flex items-center justify-between">
|
||||
<h5 key="panel-title" className="mb-0">
|
||||
Connection string
|
||||
</h5>
|
||||
{readReplicasEnabled && (
|
||||
<DatabaseSelector
|
||||
selectedDatabaseId={selectedDatabaseId}
|
||||
onChangeDatabaseId={setSelectedDatabaseId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
className="!m-0"
|
||||
>
|
||||
<Panel.Content>
|
||||
<Tabs type="underlined" size="small">
|
||||
<Tabs.Panel id="psql" label="PSQL">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={psqlConnString}
|
||||
onCopy={() => {
|
||||
handleCopy('PSQL')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
{isLoading && <ShimmeringLoader className="h-8 w-full" />}
|
||||
{isError && (
|
||||
<AlertError error={error} subject="Failed to retrieve database settings" />
|
||||
)}
|
||||
{isSuccess && (
|
||||
<Tabs type="underlined" size="small">
|
||||
<Tabs.Panel id="psql" label="PSQL">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={psqlConnString}
|
||||
onCopy={() => {
|
||||
handleCopy('PSQL')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="uri" label="URI">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={uriConnString}
|
||||
onCopy={() => {
|
||||
handleCopy('URI')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="uri" label="URI">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={uriConnString}
|
||||
onCopy={() => {
|
||||
handleCopy('URI')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="golang" label="Golang">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={golangConnString}
|
||||
onCopy={() => {
|
||||
handleCopy('Golang')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="golang" label="Golang">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={golangConnString}
|
||||
onCopy={() => {
|
||||
handleCopy('Golang')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="jdbc" label="JDBC">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={
|
||||
`jdbc:postgresql://${
|
||||
connectionInfo.db_host
|
||||
}:${connectionInfo.db_port.toString()}` +
|
||||
`/${connectionInfo.db_name}?user=${connectionInfo.db_user}&password=[YOUR-PASSWORD]`
|
||||
}
|
||||
onCopy={() => {
|
||||
handleCopy('JDBC')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="jdbc" label="JDBC">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={
|
||||
`jdbc:postgresql://${
|
||||
connectionInfo.db_host
|
||||
}:${connectionInfo.db_port.toString()}` +
|
||||
`/${connectionInfo.db_name}?user=${connectionInfo.db_user}&password=[YOUR-PASSWORD]`
|
||||
}
|
||||
onCopy={() => {
|
||||
handleCopy('JDBC')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="dotnet" label=".NET">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={
|
||||
`User Id=${connectionInfo.db_user};Password=[YOUR-PASSWORD];` +
|
||||
`Server=${
|
||||
connectionInfo.db_host
|
||||
};Port=${connectionInfo.db_port.toString()};` +
|
||||
`Database=${connectionInfo.db_name}`
|
||||
}
|
||||
onCopy={() => {
|
||||
handleCopy('.NET')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="dotnet" label=".NET">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={
|
||||
`User Id=${connectionInfo.db_user};Password=[YOUR-PASSWORD];` +
|
||||
`Server=${
|
||||
connectionInfo.db_host
|
||||
};Port=${connectionInfo.db_port.toString()};` +
|
||||
`Database=${connectionInfo.db_name}`
|
||||
}
|
||||
onCopy={() => {
|
||||
handleCopy('.NET')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="nodejs" label="Nodejs">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={uriConnString}
|
||||
onCopy={() => {
|
||||
handleCopy('Nodejs')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="nodejs" label="Nodejs">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={uriConnString}
|
||||
onCopy={() => {
|
||||
handleCopy('Nodejs')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="php" label="PHP">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={golangConnString}
|
||||
onCopy={() => {
|
||||
handleCopy('PHP')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel id="php" label="PHP">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={golangConnString}
|
||||
onCopy={() => {
|
||||
handleCopy('PHP')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel id="python" label="Python">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={
|
||||
`user=${connectionInfo.db_user} password=[YOUR-PASSWORD]` +
|
||||
` host=${connectionInfo.db_host} port=${connectionInfo.db_port.toString()}` +
|
||||
` database=${connectionInfo.db_name}`
|
||||
}
|
||||
onCopy={() => {
|
||||
handleCopy('Python')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
<Tabs.Panel id="python" label="Python">
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
disabled
|
||||
value={
|
||||
`user=${connectionInfo.db_user} password=[YOUR-PASSWORD]` +
|
||||
` host=${
|
||||
connectionInfo.db_host
|
||||
} port=${connectionInfo.db_port.toString()}` +
|
||||
` database=${connectionInfo.db_name}`
|
||||
}
|
||||
onCopy={() => {
|
||||
handleCopy('Python')
|
||||
}}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
)}
|
||||
</Panel.Content>
|
||||
</Panel>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<ConfirmDisableReadOnlyModeModal
|
||||
visible={showConfirmationModal}
|
||||
onClose={() => setShowConfirmationModal(false)}
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ const ResetDbPassword = ({ disabled = false }) => {
|
||||
</div>
|
||||
<div className="flex items-end justify-end">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
type="default"
|
||||
disabled={!canResetDbPassword || !isProjectActive || disabled}
|
||||
|
||||
@@ -101,7 +101,7 @@ const DiskSizeConfiguration = ({ disabled = false }: DiskSizeConfigurationProps)
|
||||
</div>
|
||||
<div className="flex items-end justify-end">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button
|
||||
type="default"
|
||||
disabled={!canUpdateDiskSizeConfig || disabled}
|
||||
|
||||
@@ -132,7 +132,7 @@ const SSLConfiguration = () => {
|
||||
<IconLoader className="animate-spin" strokeWidth={1.5} size={16} />
|
||||
)}
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Toggle
|
||||
checked={isEnforced}
|
||||
disabled={
|
||||
@@ -182,7 +182,7 @@ const SSLConfiguration = () => {
|
||||
</div>
|
||||
<div className="flex items-end justify-end">
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Tooltip.Trigger asChild>
|
||||
<Button type="default" disabled={!hasSSLCertificate} icon={<IconDownload />}>
|
||||
<a
|
||||
href={`https://supabase-downloads.s3-ap-southeast-1.amazonaws.com/${env}/ssl/${env}-ca-2021.crt`}
|
||||
|
||||
+12
-2
@@ -26,14 +26,18 @@ import { useInfraMonitoringQuery } from 'data/analytics/infra-monitoring-query'
|
||||
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { useResourceWarningsQuery } from 'data/usage/resource-warnings-query'
|
||||
import { useSelectedOrganization } from 'hooks'
|
||||
import { useFlag, useSelectedOrganization } from 'hooks'
|
||||
import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from 'lib/constants'
|
||||
import { INFRA_ACTIVITY_METRICS } from './Infrastructure.constants'
|
||||
import DatabaseSelector from 'components/ui/DatabaseSelector'
|
||||
|
||||
const InfrastructureActivity = () => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const organization = useSelectedOrganization()
|
||||
const [dateRange, setDateRange] = useState<any>()
|
||||
const [selectedDatabaseId, setSelectedDatabaseId] = useState<string>('1')
|
||||
|
||||
const readReplicasEnabled = useFlag('readReplicas')
|
||||
|
||||
const { data: subscription, isLoading: isLoadingSubscription } = useOrgSubscriptionQuery({
|
||||
orgSlug: organization?.slug,
|
||||
@@ -182,7 +186,13 @@ const InfrastructureActivity = () => {
|
||||
</div>
|
||||
</ScaffoldContainer>
|
||||
<ScaffoldContainer className="sticky top-0 py-6 border-b bg-background z-10">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="flex items-center gap-x-4">
|
||||
{readReplicasEnabled && (
|
||||
<DatabaseSelector
|
||||
selectedDatabaseId={selectedDatabaseId}
|
||||
onChangeDatabaseId={setSelectedDatabaseId}
|
||||
/>
|
||||
)}
|
||||
{!isLoadingSubscription && (
|
||||
<>
|
||||
<DateRangePicker
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import { useParams } from 'common'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Listbox, SidePanel } from 'ui'
|
||||
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { AVAILABLE_REPLICA_REGIONS } from './InstanceConfiguration.constants'
|
||||
import { AWS_REGIONS, AWS_REGIONS_DEFAULT, AWS_REGIONS_KEYS, BASE_PATH } from 'lib/constants'
|
||||
|
||||
// [Joshen] FYI this is purely for AWS only, need to update to support Fly eventually
|
||||
|
||||
interface DeployNewReplicaPanelProps {
|
||||
visible: boolean
|
||||
selectedDefaultRegion?: AWS_REGIONS_KEYS
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const DeployNewReplicaPanel = ({
|
||||
visible,
|
||||
selectedDefaultRegion,
|
||||
onClose,
|
||||
}: DeployNewReplicaPanelProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: addons, isSuccess } = useProjectAddonsQuery({ projectRef })
|
||||
const computeAddons =
|
||||
addons?.available_addons.find((addon) => addon.type === 'compute_instance')?.variants ?? []
|
||||
|
||||
// Opting for useState temporarily as Listbox doesn't seem to work with react-hook-form yet
|
||||
const [defaultRegion] =
|
||||
Object.entries(AWS_REGIONS).find(([key, name]) => name === AWS_REGIONS_DEFAULT) ?? []
|
||||
const defaultCompute = computeAddons.find((option) => option.name === 'Small')?.identifier
|
||||
|
||||
const [selectedRegion, setSelectedRegion] = useState(defaultRegion)
|
||||
const [selectedCompute, setSelectedCompute] = useState(defaultCompute)
|
||||
const selectedComputeMeta = computeAddons.find((addon) => addon.identifier === selectedCompute)
|
||||
|
||||
const onSubmit = async () => {
|
||||
console.log('Deploy', { selectedRegion, selectedCompute })
|
||||
onClose()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && isSuccess) {
|
||||
if (selectedDefaultRegion !== undefined) {
|
||||
setSelectedRegion(selectedDefaultRegion)
|
||||
} else if (defaultRegion) {
|
||||
setSelectedRegion(defaultRegion)
|
||||
}
|
||||
if (defaultCompute !== undefined) setSelectedCompute(defaultCompute)
|
||||
}
|
||||
}, [visible, isSuccess])
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
onConfirm={() => onSubmit()}
|
||||
header="Deploy a new read replica"
|
||||
>
|
||||
<SidePanel.Content className="flex flex-col py-4 gap-y-8">
|
||||
<Listbox
|
||||
size="small"
|
||||
id="region"
|
||||
name="region"
|
||||
value={selectedRegion}
|
||||
onChange={setSelectedRegion}
|
||||
label="Select a region to deploy your read replica in"
|
||||
>
|
||||
{AVAILABLE_REPLICA_REGIONS.map((region) => (
|
||||
<Listbox.Option
|
||||
key={region.key}
|
||||
label={region.name}
|
||||
value={region.key}
|
||||
addOnBefore={() => (
|
||||
<img
|
||||
alt="region icon"
|
||||
className="w-5 rounded-sm"
|
||||
src={`${BASE_PATH}/img/regions/${region.key}.svg`}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{region.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
|
||||
<Listbox
|
||||
size="small"
|
||||
id="compute"
|
||||
name="compute"
|
||||
value={selectedCompute}
|
||||
onChange={setSelectedCompute}
|
||||
label="Select the instance size for your read replica"
|
||||
>
|
||||
{computeAddons.map((option) => (
|
||||
<Listbox.Option key={option.identifier} label={option.name} value={option.identifier}>
|
||||
{option.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
|
||||
<p className="text-xs text-foreground-light">
|
||||
Show some preview info on cost for deploying this replica here
|
||||
</p>
|
||||
</SidePanel.Content>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
export default DeployNewReplicaPanel
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
import { AWS_REGIONS, AWS_REGIONS_KEYS } from 'lib/constants'
|
||||
|
||||
export interface DatabaseConfiguration {
|
||||
id: number
|
||||
type: 'PRIMARY' | 'READ_REPLICA'
|
||||
cloud_provider: 'AWS' | 'FLY'
|
||||
region: string
|
||||
size: string
|
||||
status: string
|
||||
inserted_at: string
|
||||
}
|
||||
|
||||
export interface Region {
|
||||
key: AWS_REGIONS_KEYS
|
||||
name: string
|
||||
region: string
|
||||
coordinates: [number, number]
|
||||
}
|
||||
|
||||
// ReactFlow is scaling everything by the factor of 2
|
||||
export const NODE_WIDTH = 560
|
||||
export const NODE_ROW_HEIGHT = 50
|
||||
export const NODE_SEP = 20
|
||||
|
||||
export const MOCK_DATABASES: DatabaseConfiguration[] = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'PRIMARY',
|
||||
cloud_provider: 'AWS',
|
||||
region: 'ap-southeast-1b',
|
||||
size: 't4g.micro',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
inserted_at: '2023-11-01 06:47:46.837002',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'READ_REPLICA',
|
||||
cloud_provider: 'AWS',
|
||||
region: 'ap-northeast-1',
|
||||
size: 't4g.micro',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
inserted_at: '2023-11-01 06:47:46.837002',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'READ_REPLICA',
|
||||
cloud_provider: 'AWS',
|
||||
region: 'ap-southeast-1b',
|
||||
size: 't4g.micro',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
inserted_at: '2023-11-01 06:47:46.837002',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'READ_REPLICA',
|
||||
cloud_provider: 'AWS',
|
||||
region: 'ap-northeast-2',
|
||||
size: 't4g.micro',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
inserted_at: '2023-11-01 06:47:46.837002',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 'READ_REPLICA',
|
||||
cloud_provider: 'AWS',
|
||||
region: 'eu-central-1',
|
||||
size: 't4g.micro',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
inserted_at: '2023-11-01 06:47:46.837002',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
type: 'READ_REPLICA',
|
||||
cloud_provider: 'AWS',
|
||||
region: 'eu-central-1',
|
||||
size: 't4g.micro',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
inserted_at: '2023-11-01 06:47:46.837002',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
type: 'READ_REPLICA',
|
||||
cloud_provider: 'AWS',
|
||||
region: 'ap-southeast-1',
|
||||
size: 't4g.micro',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
inserted_at: '2023-11-01 06:47:46.837002',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
type: 'READ_REPLICA',
|
||||
cloud_provider: 'AWS',
|
||||
region: 'ap-southeast-1',
|
||||
size: 't4g.micro',
|
||||
status: 'ACTIVE_HEALTHY',
|
||||
inserted_at: '2023-11-01 06:47:46.837002',
|
||||
},
|
||||
]
|
||||
|
||||
// [Joshen] Coordinates from https://github.com/jsonmaur/aws-regions/issues/11
|
||||
const AWS_REGIONS_COORDINATES: { [key: string]: [number, number] } = {
|
||||
SOUTHEAST_ASIA: [103.8, 1.37],
|
||||
NORTHEAST_ASIA: [139.42, 35.41],
|
||||
NORTHEAST_ASIA_2: [126.98, 37.56],
|
||||
CENTRAL_CANADA: [-73.6, 45.5],
|
||||
WEST_US: [-121.96, 37.35],
|
||||
EAST_US: [-78.45, 38.13],
|
||||
WEST_EU: [-8, 53],
|
||||
WEST_EU_2: [-0.1, 51],
|
||||
CENTRAL_EU: [8, 50],
|
||||
SOUTH_ASIA: [72.88, 19.08],
|
||||
OCEANIA: [151.2, -33.86],
|
||||
SOUTH_AMERICA: [-46.38, -23.34],
|
||||
}
|
||||
|
||||
const AWS_REGIONS_VALUES: { [key: string]: string } = {
|
||||
SOUTHEAST_ASIA: 'ap-southeast-1',
|
||||
NORTHEAST_ASIA: 'ap-northeast-1',
|
||||
NORTHEAST_ASIA_2: 'ap-northeast-2',
|
||||
CENTRAL_CANADA: 'ca-central-1',
|
||||
WEST_US: 'us-west-1',
|
||||
EAST_US: 'es-east-1',
|
||||
WEST_EU: 'eu-west-1',
|
||||
WEST_EU_2: 'eu-west-2',
|
||||
CENTRAL_EU: 'eu-central-1',
|
||||
SOUTH_ASIA: 'ap-south-1',
|
||||
OCEANIA: 'ap-southeast-2',
|
||||
SOUTH_AMERICA: 'sa-east-1',
|
||||
}
|
||||
|
||||
// [Joshen] Just to make sure that we just depend on AWS_REGIONS to determine available
|
||||
// regions for replicas. Just FYI - might need to update this if we support Fly in future
|
||||
export const AVAILABLE_REPLICA_REGIONS: Region[] = Object.keys(AWS_REGIONS)
|
||||
.map((key) => {
|
||||
return {
|
||||
key: key as AWS_REGIONS_KEYS,
|
||||
name: AWS_REGIONS[key as AWS_REGIONS_KEYS],
|
||||
region: AWS_REGIONS_VALUES[key],
|
||||
coordinates: AWS_REGIONS_COORDINATES[key],
|
||||
}
|
||||
})
|
||||
.filter((x) => x.coordinates !== undefined)
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
import { useParams } from 'common'
|
||||
import { partition } from 'lodash'
|
||||
import { Globe2, Network } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useMemo, useState } from 'react'
|
||||
import ReactFlow, { Background, Edge, ReactFlowProvider, useReactFlow } from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import {
|
||||
AlertDescription_Shadcn_,
|
||||
AlertTitle_Shadcn_,
|
||||
Alert_Shadcn_,
|
||||
Button,
|
||||
IconAlertTriangle,
|
||||
Modal,
|
||||
} from 'ui'
|
||||
|
||||
import ConfirmationModal from 'components/ui/ConfirmationModal'
|
||||
import { AWS_REGIONS_KEYS } from 'lib/constants'
|
||||
import DeployNewReplicaPanel from './DeployNewReplicaPanel'
|
||||
import { DatabaseConfiguration, MOCK_DATABASES } from './InstanceConfiguration.constants'
|
||||
import { addRegionNodes, generateNodes, getDagreGraphLayout } from './InstanceConfiguration.utils'
|
||||
import { PrimaryNode, RegionNode, ReplicaNode } from './InstanceNode'
|
||||
import MapView from './MapView'
|
||||
import ResizeReplicaPanel from './ResizeReplicaPanel'
|
||||
|
||||
// [Joshen] Just FYI, UI assumes single provider for primary + replicas
|
||||
// [Joshen] Idea to visualize grouping based on region: https://reactflow.dev/examples/layout/sub-flows
|
||||
// [Joshen] Show flags for regions
|
||||
|
||||
const InstanceConfigurationUI = () => {
|
||||
const reactFlow = useReactFlow()
|
||||
const { resolvedTheme } = useTheme()
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const [view, setView] = useState<'flow' | 'map'>('flow')
|
||||
const [showNewReplicaPanel, setShowNewReplicaPanel] = useState(false)
|
||||
const [newReplicaRegion, setNewReplicaRegion] = useState<AWS_REGIONS_KEYS>()
|
||||
const [selectedReplicaToResize, setSelectedReplicaToResize] = useState<DatabaseConfiguration>()
|
||||
const [selectedReplicaToDrop, setSelectedReplicaToDrop] = useState<DatabaseConfiguration>()
|
||||
const [selectedReplicaToRestart, setSelectedReplicaToRestart] = useState<DatabaseConfiguration>()
|
||||
|
||||
const backgroundPatternColor =
|
||||
resolvedTheme === 'dark' ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.4)'
|
||||
|
||||
const [[primary], replicas] = partition(MOCK_DATABASES, (database) => database.type === 'PRIMARY')
|
||||
|
||||
const nodes = generateNodes(MOCK_DATABASES, {
|
||||
onSelectRestartReplica: setSelectedReplicaToRestart,
|
||||
onSelectResizeReplica: setSelectedReplicaToResize,
|
||||
onSelectDropReplica: setSelectedReplicaToDrop,
|
||||
})
|
||||
|
||||
const edges: Edge[] = replicas.map((database) => {
|
||||
return {
|
||||
id: `${primary.id}-${database.id}`,
|
||||
source: `database-${primary.id}`,
|
||||
target: `database-${database.id}`,
|
||||
type: 'smoothstep',
|
||||
animated: true,
|
||||
}
|
||||
})
|
||||
|
||||
const nodeTypes = useMemo(
|
||||
() => ({ PRIMARY: PrimaryNode, READ_REPLICA: ReplicaNode, REGION: RegionNode }),
|
||||
[]
|
||||
)
|
||||
|
||||
const onConfirmDropReplica = () => {
|
||||
console.log('Drop replica', selectedReplicaToDrop)
|
||||
}
|
||||
|
||||
const onConfirmRestartReplica = () => {
|
||||
console.log('Restart replica', selectedReplicaToRestart)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-[500px] w-full relative">
|
||||
<div className="z-10 absolute top-4 right-4 flex items-center justify-center gap-x-2">
|
||||
<Button type="default" onClick={() => setShowNewReplicaPanel(true)}>
|
||||
Deploy a new replica
|
||||
</Button>
|
||||
<div className="flex items-center justify-center">
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Network size={15} />}
|
||||
className={`rounded-r-none transition ${
|
||||
view === 'flow' ? 'opacity-100' : 'opacity-50'
|
||||
}`}
|
||||
onClick={() => setView('flow')}
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Globe2 size={15} />}
|
||||
className={`rounded-l-none transition ${
|
||||
view === 'map' ? 'opacity-100' : 'opacity-50'
|
||||
}`}
|
||||
onClick={() => setView('map')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{view === 'flow' ? (
|
||||
<ReactFlow
|
||||
fitView
|
||||
fitViewOptions={{ minZoom: 0.9, maxZoom: 1 }}
|
||||
className="instance-configuration"
|
||||
zoomOnPinch={false}
|
||||
nodesDraggable={false}
|
||||
nodesConnectable={false}
|
||||
zoomOnDoubleClick={false}
|
||||
edgesFocusable={false}
|
||||
edgesUpdatable={false}
|
||||
defaultNodes={[]}
|
||||
defaultEdges={[]}
|
||||
nodeTypes={nodeTypes}
|
||||
onInit={() => {
|
||||
const graph = getDagreGraphLayout(nodes, edges)
|
||||
const xxx = addRegionNodes(graph.nodes, graph.edges)
|
||||
reactFlow.setNodes(xxx.nodes)
|
||||
reactFlow.setEdges(graph.edges)
|
||||
}}
|
||||
proOptions={{ hideAttribution: true }}
|
||||
>
|
||||
<Background color={backgroundPatternColor} />
|
||||
</ReactFlow>
|
||||
) : (
|
||||
<MapView
|
||||
onSelectDeployNewReplica={(region) => {
|
||||
setNewReplicaRegion(region)
|
||||
setShowNewReplicaPanel(true)
|
||||
}}
|
||||
onSelectRestartReplica={setSelectedReplicaToRestart}
|
||||
onSelectResizeReplica={setSelectedReplicaToResize}
|
||||
onSelectDropReplica={setSelectedReplicaToDrop}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DeployNewReplicaPanel
|
||||
visible={showNewReplicaPanel}
|
||||
selectedDefaultRegion={newReplicaRegion}
|
||||
onClose={() => {
|
||||
setNewReplicaRegion(undefined)
|
||||
setShowNewReplicaPanel(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
<ResizeReplicaPanel
|
||||
visible={selectedReplicaToResize !== undefined}
|
||||
selectedReplica={selectedReplicaToResize}
|
||||
onClose={() => setSelectedReplicaToResize(undefined)}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
danger
|
||||
size="medium"
|
||||
visible={selectedReplicaToDrop !== undefined}
|
||||
header="Confirm to drop selected replica?"
|
||||
buttonLabel="Drop replica"
|
||||
buttonLoadingLabel="Dropping replica"
|
||||
onSelectCancel={() => setSelectedReplicaToDrop(undefined)}
|
||||
onSelectConfirm={() => onConfirmDropReplica()}
|
||||
>
|
||||
<Modal.Content className="py-3">
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<IconAlertTriangle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>This action cannot be undone</AlertTitle_Shadcn_>
|
||||
<AlertDescription_Shadcn_>
|
||||
You may still deploy a new replica in this region thereafter
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
<div className="text-sm px-1 pt-4">
|
||||
<p>Before deleting this replica, consider:</p>
|
||||
<ul className="text-foreground-light py-1 list-disc mx-4 space-y-1">
|
||||
<li>
|
||||
Network traffic from this region may slow down, especially if you have no other
|
||||
replicas in this region
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ConfirmationModal
|
||||
size="medium"
|
||||
visible={selectedReplicaToRestart !== undefined}
|
||||
header="Confirm to restart selected replica?"
|
||||
buttonLabel="Restart replica"
|
||||
buttonLoadingLabel="Restarting replica"
|
||||
onSelectCancel={() => setSelectedReplicaToRestart(undefined)}
|
||||
onSelectConfirm={() => onConfirmRestartReplica()}
|
||||
>
|
||||
<Modal.Content className="py-3">
|
||||
<p className="text-sm">Before restarting the replica, consider:</p>
|
||||
<ul className="text-sm text-foreground-light py-1 list-disc mx-4 space-y-1">
|
||||
<li>
|
||||
Network traffic from this region may slow down while the replica is restarting,
|
||||
especially if you have no other replicas in this region
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-sm mt-2">
|
||||
Are you sure you want to restart this replica (ID: {selectedReplicaToRestart?.id}) now?{' '}
|
||||
</p>
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const InstanceConfiguration = () => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<InstanceConfigurationUI />
|
||||
</ReactFlowProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default InstanceConfiguration
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
import dagre from '@dagrejs/dagre'
|
||||
import { Edge, Node, Position } from 'reactflow'
|
||||
|
||||
import {
|
||||
AVAILABLE_REPLICA_REGIONS,
|
||||
DatabaseConfiguration,
|
||||
NODE_ROW_HEIGHT,
|
||||
NODE_SEP,
|
||||
NODE_WIDTH,
|
||||
} from './InstanceConfiguration.constants'
|
||||
import { groupBy } from 'lodash'
|
||||
|
||||
export const generateNodes = (
|
||||
databases: DatabaseConfiguration[],
|
||||
{
|
||||
onSelectRestartReplica,
|
||||
onSelectResizeReplica,
|
||||
onSelectDropReplica,
|
||||
}: {
|
||||
onSelectRestartReplica: (database: DatabaseConfiguration) => void
|
||||
onSelectResizeReplica: (database: DatabaseConfiguration) => void
|
||||
onSelectDropReplica: (database: DatabaseConfiguration) => void
|
||||
}
|
||||
): Node[] => {
|
||||
const position = { x: 0, y: 0 }
|
||||
const replicas = databases.filter((d) => d.type === 'READ_REPLICA')
|
||||
const regions = groupBy(replicas, (d) => {
|
||||
const region = AVAILABLE_REPLICA_REGIONS.find((region) => d.region.includes(region.region))
|
||||
return region?.key
|
||||
})
|
||||
|
||||
const databaseNodes: Node[] = databases
|
||||
.sort((a, b) => (a.region > b.region ? 1 : -1))
|
||||
.map((database) => {
|
||||
const region = AVAILABLE_REPLICA_REGIONS.find((region) =>
|
||||
database.region.includes(region.region)
|
||||
)
|
||||
|
||||
return {
|
||||
position,
|
||||
id: `database-${database.id}`,
|
||||
type: database.type,
|
||||
data: {
|
||||
id: database.id,
|
||||
region,
|
||||
label: database.type === 'PRIMARY' ? 'Primary Database' : 'Read Replica',
|
||||
provider: database.cloud_provider,
|
||||
inserted_at: database.inserted_at,
|
||||
computeSize: database.size,
|
||||
...(database.type === 'READ_REPLICA'
|
||||
? {
|
||||
onSelectRestartReplica: () => onSelectRestartReplica(database),
|
||||
onSelectResizeReplica: () => onSelectResizeReplica(database),
|
||||
onSelectDropReplica: () => onSelectDropReplica(database),
|
||||
}
|
||||
: {}),
|
||||
...(database.type === 'PRIMARY'
|
||||
? {
|
||||
numReplicas: replicas.length,
|
||||
numRegions: Object.keys(regions).length,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return [...databaseNodes]
|
||||
}
|
||||
|
||||
export const getDagreGraphLayout = (nodes: Node[], edges: Edge[]) => {
|
||||
const dagreGraph = new dagre.graphlib.Graph()
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
||||
dagreGraph.setGraph({ rankdir: 'TB', ranksep: 200, nodesep: NODE_SEP })
|
||||
|
||||
nodes.forEach((node) =>
|
||||
dagreGraph.setNode(node.id, { width: NODE_WIDTH / 2, height: NODE_ROW_HEIGHT / 2 })
|
||||
)
|
||||
|
||||
edges.forEach((edge) => dagreGraph.setEdge(edge.source, edge.target))
|
||||
|
||||
dagre.layout(dagreGraph)
|
||||
|
||||
nodes.forEach((node) => {
|
||||
const nodeWithPosition = dagreGraph.node(node.id)
|
||||
node.targetPosition = Position.Top
|
||||
node.sourcePosition = Position.Bottom
|
||||
// We are shifting the dagre node position (anchor=center center) to the top left
|
||||
// so it matches the React Flow node anchor point (top left).
|
||||
node.position = {
|
||||
x: nodeWithPosition.x - nodeWithPosition.width / 2,
|
||||
y: nodeWithPosition.y - nodeWithPosition.height / 2,
|
||||
}
|
||||
|
||||
return node
|
||||
})
|
||||
|
||||
return { nodes, edges }
|
||||
}
|
||||
|
||||
/**
|
||||
* [Joshen] This is some customized logic to add region nodes as "subflow" as dagre doesn't support
|
||||
* subflows, and I didn't want to go down a rabbit hole with the other layout libraries that react-flow
|
||||
* supports. Definitely some things to improve in the future
|
||||
* - Allow setting max number of nodes per row, so that the chart does not become too horizontally sparse
|
||||
* when many many replicas created
|
||||
* - Nodes are a bit too spaced out between each other within a region
|
||||
*/
|
||||
export const addRegionNodes = (nodes: Node[], edges: Edge[]) => {
|
||||
const regionNodes: Node[] = []
|
||||
const replicaNodes = nodes.filter((node) => node.type === 'READ_REPLICA')
|
||||
|
||||
const nodesByRegion = groupBy(replicaNodes, (node) => node.data.region.key)
|
||||
Object.entries(nodesByRegion).map(([key, value]) => {
|
||||
const region = AVAILABLE_REPLICA_REGIONS.find((r) => r.key === key)
|
||||
const nodeXPositions = value.map((x) => x.position.x)
|
||||
const nodeYPositions = value.map((x) => x.position.y)
|
||||
|
||||
const minX = Math.min(...nodeXPositions)
|
||||
const maxX = Math.max(...nodeXPositions)
|
||||
|
||||
const minY = Math.max(...nodeYPositions)
|
||||
|
||||
const regionNode: Node = {
|
||||
id: key,
|
||||
position: { x: minX - 10, y: minY - 10 },
|
||||
width: maxX - minX,
|
||||
type: 'REGION',
|
||||
data: { region, numReplicas: value.length },
|
||||
}
|
||||
regionNodes.push(regionNode)
|
||||
})
|
||||
|
||||
return { nodes: [...regionNodes, ...nodes], edges }
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
import { useParams } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { Database, DatabaseBackup } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { Handle, NodeProps, Position } from 'reactflow'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconMoreVertical,
|
||||
} from 'ui'
|
||||
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { NODE_SEP, NODE_WIDTH, Region } from './InstanceConfiguration.constants'
|
||||
|
||||
interface NodeData {
|
||||
id: string
|
||||
label: string
|
||||
provider: string
|
||||
region: Region
|
||||
computeSize: string
|
||||
inserted_at: string
|
||||
}
|
||||
|
||||
interface PrimaryNodeData extends NodeData {
|
||||
numReplicas: number
|
||||
numRegions: number
|
||||
}
|
||||
|
||||
interface ReplicaNodeData extends NodeData {
|
||||
onSelectRestartReplica: () => void
|
||||
onSelectResizeReplica: () => void
|
||||
onSelectDropReplica: () => void
|
||||
}
|
||||
|
||||
export const PrimaryNode = ({ data }: NodeProps<PrimaryNodeData>) => {
|
||||
const { label, provider, region, computeSize, numReplicas, numRegions } = data
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col rounded bg-surface-100 border border-default">
|
||||
<div
|
||||
className="flex items-start justify-between p-3"
|
||||
style={{ width: NODE_WIDTH / 2 - 10 }}
|
||||
>
|
||||
<div className="flex gap-x-3">
|
||||
<div className="w-8 h-8 bg-brand-500 border border-brand-600 rounded-md flex items-center justify-center">
|
||||
<Database size={16} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<p className="text-sm">{label}</p>
|
||||
<p className="flex items-center gap-x-1">
|
||||
<span className="text-xs text-foreground-light">{region.name}</span>
|
||||
</p>
|
||||
<p className="flex items-center gap-x-1">
|
||||
<span className="text-xs text-foreground-light">{provider}</span>
|
||||
<span className="text-xs text-foreground-light">•</span>
|
||||
<span className="text-xs text-foreground-light">{computeSize}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
alt="region icon"
|
||||
className="w-8 rounded-sm mt-0.5"
|
||||
src={`${BASE_PATH}/img/regions/${region.key}.svg`}
|
||||
/>
|
||||
</div>
|
||||
{numReplicas > 0 && (
|
||||
<div className="border-t p-3 py-2">
|
||||
<p className="text-sm text-foreground-light">
|
||||
<span className="text-foreground">{numReplicas} replicas</span> deployed across{' '}
|
||||
<span className="text-foreground">{numRegions} regions</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Handle
|
||||
type="source"
|
||||
id="handle-b"
|
||||
position={Position.Bottom}
|
||||
style={{ background: 'transparent' }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const ReplicaNode = ({ data }: NodeProps<ReplicaNodeData>) => {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
provider,
|
||||
region,
|
||||
computeSize,
|
||||
inserted_at,
|
||||
onSelectRestartReplica,
|
||||
onSelectResizeReplica,
|
||||
onSelectDropReplica,
|
||||
} = data
|
||||
const { ref } = useParams()
|
||||
const created = dayjs(inserted_at).format('DD MMM YYYY')
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle
|
||||
type="target"
|
||||
id="handle-t"
|
||||
position={Position.Top}
|
||||
style={{ background: 'transparent' }}
|
||||
/>
|
||||
<div
|
||||
className="flex justify-between rounded bg-surface-100 border border-default p-3"
|
||||
style={{ width: NODE_WIDTH / 2 - 10 }}
|
||||
>
|
||||
<div className="flex gap-x-3">
|
||||
<div className="w-8 h-8 bg-brand-400 border border-brand-500 rounded-md flex items-center justify-center">
|
||||
<DatabaseBackup size={16} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p className="text-sm">
|
||||
{label} {id}
|
||||
</p>
|
||||
{/* [Joshen] Some status indication perhaps */}
|
||||
<Badge color="green">Healthy</Badge>
|
||||
</div>
|
||||
<div className="my-0.5">
|
||||
<p className="flex text-xs text-foreground-light items-center gap-x-1">
|
||||
{region.name}
|
||||
</p>
|
||||
<p className="flex text-xs text-foreground-light items-center gap-x-1">
|
||||
<span>{provider}</span>
|
||||
<span>•</span>
|
||||
<span>{computeSize}</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-foreground-light">Created: {created}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="text" icon={<IconMoreVertical />} className="px-1" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0 w-40" side="bottom" align="end">
|
||||
<DropdownMenuItem className="gap-x-2">
|
||||
<Link href={`/project/${ref}/settings/database?connectionString=${id}`}>
|
||||
View connection string
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="gap-x-2" onClick={() => onSelectRestartReplica()}>
|
||||
Restart replica
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="gap-x-2" onClick={() => onSelectResizeReplica()}>
|
||||
Resize replica
|
||||
</DropdownMenuItem>
|
||||
<div className="border-t" />
|
||||
<DropdownMenuItem className="gap-x-2" onClick={() => onSelectDropReplica()}>
|
||||
Drop replica
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const RegionNode = ({ data }: any) => {
|
||||
const { region, numReplicas } = data
|
||||
const regionNodeWidth =
|
||||
20 + (NODE_WIDTH / 2 - 10) * numReplicas + (numReplicas - 1) * (NODE_SEP + 10)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex justify-between rounded bg-black/10 border border-default border-white/10 border-2 p-3"
|
||||
style={{ width: regionNodeWidth, height: 150 }}
|
||||
>
|
||||
<div className="absolute bottom-2 flex items-center justify-between gap-x-2">
|
||||
<img
|
||||
alt="region icon"
|
||||
className="w-5 rounded-sm"
|
||||
src={`${BASE_PATH}/img/regions/${region.key}.svg`}
|
||||
/>
|
||||
<p className="text-sm">{region.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
const InstanceResizeModal = () => {
|
||||
return <div>Hello</div>
|
||||
}
|
||||
|
||||
export default InstanceResizeModal
|
||||
+11241
File diff suppressed because it is too large.
Load diff
+331
@@ -0,0 +1,331 @@
|
||||
import { useParams } from 'common'
|
||||
import dayjs from 'dayjs'
|
||||
import { partition, uniqBy } from 'lodash'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
ComposableMap,
|
||||
Geographies,
|
||||
Geography,
|
||||
Line,
|
||||
Marker,
|
||||
ZoomableGroup,
|
||||
} from 'react-simple-maps'
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
IconMoreVertical,
|
||||
ScrollArea,
|
||||
} from 'ui'
|
||||
|
||||
import { AWS_REGIONS_KEYS, BASE_PATH } from 'lib/constants'
|
||||
import {
|
||||
AVAILABLE_REPLICA_REGIONS,
|
||||
DatabaseConfiguration,
|
||||
MOCK_DATABASES,
|
||||
} from './InstanceConfiguration.constants'
|
||||
import GeographyData from './MapData.json'
|
||||
|
||||
// [Joshen] Foresee that we'll skip this view for initial launch
|
||||
|
||||
interface MapViewProps {
|
||||
onSelectDeployNewReplica: (region: AWS_REGIONS_KEYS) => void
|
||||
onSelectRestartReplica: (database: DatabaseConfiguration) => void
|
||||
onSelectResizeReplica: (database: DatabaseConfiguration) => void
|
||||
onSelectDropReplica: (database: DatabaseConfiguration) => void
|
||||
}
|
||||
|
||||
const MapView = ({
|
||||
onSelectDeployNewReplica,
|
||||
onSelectRestartReplica,
|
||||
onSelectResizeReplica,
|
||||
onSelectDropReplica,
|
||||
}: MapViewProps) => {
|
||||
const { ref } = useParams()
|
||||
const [mount, setMount] = useState(false)
|
||||
const [zoom, setZoom] = useState<number>(1)
|
||||
const [center, setCenter] = useState<[number, number]>([14, 7])
|
||||
const [tooltip, setTooltip] = useState<{
|
||||
x: number
|
||||
y: number
|
||||
region?: { key: string; country?: string; name?: string }
|
||||
network?: any
|
||||
}>()
|
||||
|
||||
const [[primary], replicas] = partition(MOCK_DATABASES, (database) => database.type === 'PRIMARY')
|
||||
const primaryCoordinates = AVAILABLE_REPLICA_REGIONS.find((region) =>
|
||||
primary.region.includes(region.region)
|
||||
)?.coordinates ?? [0, 0]
|
||||
const uniqueRegionsByReplicas = uniqBy(replicas, (r) => {
|
||||
return AVAILABLE_REPLICA_REGIONS.find((region) => r.region.includes(region.region))?.key
|
||||
})
|
||||
|
||||
const selectedRegionKey =
|
||||
AVAILABLE_REPLICA_REGIONS.find((region) => region.coordinates === center)?.region ?? ''
|
||||
const showRegionDetails = zoom === 1.5 && selectedRegionKey !== undefined
|
||||
const selectedRegion = AVAILABLE_REPLICA_REGIONS.find(
|
||||
(region) => region.region === selectedRegionKey
|
||||
)
|
||||
const databasesInSelectedRegion = useMemo(
|
||||
() =>
|
||||
MOCK_DATABASES.filter((database) => database.region.includes(selectedRegionKey))
|
||||
.sort((a, b) => (a.id > b.id ? 1 : 0))
|
||||
.sort((database) => (database.type === 'PRIMARY' ? -1 : 0)),
|
||||
[selectedRegionKey]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => setMount(true), 100)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="bg-background">
|
||||
<ComposableMap projectionConfig={{ scale: 140 }} height={354}>
|
||||
<ZoomableGroup
|
||||
className={mount ? 'transition-all duration-300' : ''}
|
||||
center={center}
|
||||
zoom={zoom}
|
||||
minZoom={1}
|
||||
maxZoom={1.5}
|
||||
filterZoomEvent={({ constructor: { name } }) =>
|
||||
!['MouseEvent', 'WheelEvent'].includes(name)
|
||||
}
|
||||
>
|
||||
<Geographies geography={GeographyData}>
|
||||
{({ geographies }) =>
|
||||
geographies.map((geo) => (
|
||||
<Geography
|
||||
key={geo.rsmKey}
|
||||
geography={geo}
|
||||
strokeWidth={0.3}
|
||||
pointerEvents="none"
|
||||
className="fill-gray-300 stroke-gray-200"
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Geographies>
|
||||
|
||||
{uniqueRegionsByReplicas.map((database) => {
|
||||
const coordinates = AVAILABLE_REPLICA_REGIONS.find((region) =>
|
||||
database.region.includes(region.region)
|
||||
)?.coordinates
|
||||
|
||||
if (coordinates !== primaryCoordinates) {
|
||||
return (
|
||||
<Line
|
||||
key={`line-${database.id}-${primary.id}`}
|
||||
from={coordinates}
|
||||
to={primaryCoordinates}
|
||||
stroke="white"
|
||||
strokeWidth={1}
|
||||
strokeLinecap="round"
|
||||
strokeOpacity={0.2}
|
||||
strokeDasharray={'3, 3'}
|
||||
className="map-path"
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
|
||||
{AVAILABLE_REPLICA_REGIONS.map((region) => {
|
||||
const databases =
|
||||
MOCK_DATABASES.filter((database) => database.region.includes(region.region)) ?? []
|
||||
const coordinates = AVAILABLE_REPLICA_REGIONS.find(
|
||||
(r) => r.region === region.region
|
||||
)?.coordinates
|
||||
|
||||
const hasNoDatabases = databases.length === 0
|
||||
const hasPrimary = databases.some((database) => database.type === 'PRIMARY')
|
||||
const replicas = databases.filter((database) => database.type === 'READ_REPLICA') ?? []
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={region.key}
|
||||
coordinates={coordinates}
|
||||
onMouseEnter={(event) =>
|
||||
setTooltip({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
region: {
|
||||
key: region.key,
|
||||
country: region.name,
|
||||
name: hasNoDatabases
|
||||
? undefined
|
||||
: hasPrimary
|
||||
? `Primary Database${
|
||||
replicas.length > 0 ? ` + ${replicas.length} replicas` : ''
|
||||
}`
|
||||
: `${replicas.length} Read Replica${
|
||||
replicas.length > 1 ? 's' : ''
|
||||
} deployed`,
|
||||
},
|
||||
})
|
||||
}
|
||||
onMouseLeave={() => setTooltip(undefined)}
|
||||
onClick={() => {
|
||||
if (coordinates) {
|
||||
setCenter(coordinates)
|
||||
setZoom(1.5)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{selectedRegionKey === region.region && (
|
||||
<circle
|
||||
r={4}
|
||||
className={`animate-ping ${hasNoDatabases ? 'fill-white/30' : 'fill-brand'}`}
|
||||
/>
|
||||
)}
|
||||
<circle
|
||||
r={4}
|
||||
className={`cursor-pointer ${
|
||||
hasNoDatabases
|
||||
? 'fill-background-surface-300 stroke-white/20'
|
||||
: hasPrimary
|
||||
? 'fill-brand stroke-brand-500'
|
||||
: 'fill-brand-500 stroke-brand-400'
|
||||
}`}
|
||||
/>
|
||||
</Marker>
|
||||
)
|
||||
})}
|
||||
</ZoomableGroup>
|
||||
</ComposableMap>
|
||||
|
||||
{tooltip?.region !== undefined && (
|
||||
<div
|
||||
className="absolute w-[220px] bg-black bg-opacity-50 rounded border"
|
||||
style={{ left: tooltip.x - 420, top: tooltip.y - 130 }}
|
||||
>
|
||||
<div className="px-3 py-2 flex flex-col gap-y-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<img
|
||||
alt="region icon"
|
||||
className="w-5 rounded-sm"
|
||||
src={`${BASE_PATH}/img/regions/${tooltip.region.key}.svg`}
|
||||
/>
|
||||
<p className="text-xs">{tooltip.region.country}</p>
|
||||
</div>
|
||||
<p
|
||||
className={`text-xs ${
|
||||
tooltip.region.name === undefined ? 'text-foreground-light' : ''
|
||||
}`}
|
||||
>
|
||||
{tooltip.region.name ?? 'No databases deployed'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showRegionDetails && selectedRegion && (
|
||||
<div className="absolute bottom-4 right-4 flex flex-col bg-black bg-opacity-50 backdrop-blur-sm border rounded w-[400px]">
|
||||
<div className="flex items-center justify-between py-4 px-4 border-b">
|
||||
<div>
|
||||
<p className="text-xs text-foreground-light">
|
||||
{databasesInSelectedRegion.length} database
|
||||
{databasesInSelectedRegion.length > 1 ? 's' : ''} deployed in
|
||||
</p>
|
||||
<p className="text-sm">{selectedRegion.name}</p>
|
||||
</div>
|
||||
<img
|
||||
alt="region icon"
|
||||
className="w-10 rounded-sm"
|
||||
src={`${BASE_PATH}/img/regions/${selectedRegion.key}.svg`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{databasesInSelectedRegion.length > 0 && (
|
||||
<ScrollArea style={{ height: databasesInSelectedRegion.length > 2 ? '180px' : 'auto' }}>
|
||||
<ul className={`flex flex-col divide-y`}>
|
||||
{databasesInSelectedRegion.map((database) => {
|
||||
const created = dayjs(database.inserted_at).format('DD MMM YYYY, HH:mm:ss (ZZ)')
|
||||
|
||||
return (
|
||||
<li
|
||||
key={database.id}
|
||||
className="text-sm px-4 py-2 flex items-center justify-between"
|
||||
>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<p className="flex items-center gap-x-2">
|
||||
{database.type === 'PRIMARY'
|
||||
? 'Primary Database'
|
||||
: `Read Replica (ID: ${database.id})`}
|
||||
{database.type === 'READ_REPLICA' && <Badge color="green">Healthy</Badge>}
|
||||
</p>
|
||||
<p className="text-xs text-foreground-light">AWS • {database.size}</p>
|
||||
{database.type === 'READ_REPLICA' && (
|
||||
<p className="text-xs text-foreground-light">Created on: {created}</p>
|
||||
)}
|
||||
</div>
|
||||
{database.type === 'READ_REPLICA' && (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="text" icon={<IconMoreVertical />} className="px-1" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="p-0 w-40" side="bottom" align="end">
|
||||
<DropdownMenuItem className="gap-x-2">
|
||||
<Link
|
||||
href={`/project/${ref}/settings/database?connectionString=${database.id}`}
|
||||
>
|
||||
View connection string
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="gap-x-2"
|
||||
onClick={() => onSelectRestartReplica(database)}
|
||||
>
|
||||
Restart replica
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="gap-x-2"
|
||||
onClick={() => onSelectResizeReplica(database)}
|
||||
>
|
||||
Resize replica
|
||||
</DropdownMenuItem>
|
||||
<div className="border-t" />
|
||||
<DropdownMenuItem
|
||||
className="gap-x-2"
|
||||
onClick={() => onSelectDropReplica(database)}
|
||||
>
|
||||
Drop replica
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={`flex items-center justify-end gap-x-2 px-4 py-4 ${
|
||||
databasesInSelectedRegion.length > 0 ? 'border-t' : ''
|
||||
}`}
|
||||
>
|
||||
<Button type="default" onClick={() => onSelectDeployNewReplica(selectedRegion.key)}>
|
||||
Deploy new replica here
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => {
|
||||
setCenter([14, 7])
|
||||
setZoom(1)
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default MapView
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { useParams } from 'common'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Listbox, SidePanel } from 'ui'
|
||||
|
||||
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
|
||||
import { AVAILABLE_REPLICA_REGIONS, DatabaseConfiguration } from './InstanceConfiguration.constants'
|
||||
import { AWS_REGIONS, AWS_REGIONS_DEFAULT, AWS_REGIONS_KEYS, BASE_PATH } from 'lib/constants'
|
||||
|
||||
// [Joshen] FYI this is purely for AWS only, need to update to support Fly eventually
|
||||
|
||||
interface ResizeReplicaPanelProps {
|
||||
visible: boolean
|
||||
selectedReplica?: DatabaseConfiguration
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const ResizeReplicaPanel = ({ visible, selectedReplica, onClose }: ResizeReplicaPanelProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const { data: addons, isSuccess } = useProjectAddonsQuery({ projectRef })
|
||||
const computeAddons =
|
||||
addons?.available_addons.find((addon) => addon.type === 'compute_instance')?.variants ?? []
|
||||
|
||||
const defaultCompute = computeAddons.find((option) => option.name === 'Small')?.identifier
|
||||
|
||||
const [selectedCompute, setSelectedCompute] = useState(defaultCompute)
|
||||
const selectedComputeMeta = computeAddons.find((addon) => addon.identifier === selectedCompute)
|
||||
const replicaRegion = AVAILABLE_REPLICA_REGIONS.find((r) =>
|
||||
selectedReplica?.region.includes(r.region)
|
||||
)
|
||||
|
||||
const onSubmit = async () => {
|
||||
console.log('Resize', { selectedCompute })
|
||||
onClose()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && isSuccess) {
|
||||
}
|
||||
}, [visible, isSuccess])
|
||||
|
||||
return (
|
||||
<SidePanel
|
||||
visible={visible}
|
||||
onCancel={onClose}
|
||||
onConfirm={() => onSubmit()}
|
||||
header={`Resize read replica (ID: ${selectedReplica?.id})`}
|
||||
>
|
||||
<SidePanel.Content className="flex flex-col py-4 gap-y-8">
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<p className="text-sm text-foreground-light">Replica is currently deployed in</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<img
|
||||
alt="region icon"
|
||||
className="w-7 rounded-sm"
|
||||
src={`${BASE_PATH}/img/regions/${replicaRegion?.key}.svg`}
|
||||
/>
|
||||
<p>{replicaRegion?.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Listbox
|
||||
size="small"
|
||||
id="compute"
|
||||
name="compute"
|
||||
value={selectedCompute}
|
||||
onChange={setSelectedCompute}
|
||||
label="Select the instance size for your read replica"
|
||||
>
|
||||
{computeAddons.map((option) => (
|
||||
<Listbox.Option key={option.identifier} label={option.name} value={option.identifier}>
|
||||
{option.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox>
|
||||
|
||||
<p className="text-xs text-foreground-light">
|
||||
Show some preview info on cost for deploying this replica here
|
||||
</p>
|
||||
</SidePanel.Content>
|
||||
</SidePanel>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResizeReplicaPanel
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Input,
|
||||
} from 'ui'
|
||||
import ProjectUpgradeAlert from '../General/Infrastructure/ProjectUpgradeAlert'
|
||||
import InstanceConfiguration from './InfrastructureConfiguration/InstanceConfiguration'
|
||||
|
||||
const InfrastructureInfo = () => {
|
||||
const { ref } = useParams()
|
||||
@@ -45,6 +46,7 @@ const InfrastructureInfo = () => {
|
||||
const latestPgVersion = (latest_app_version ?? '').split('supabase-postgres-')[1]
|
||||
|
||||
const showDbUpgrades = useFlag('databaseUpgrades')
|
||||
const readReplicasEnabled = useFlag('readReplicas')
|
||||
const subject = 'Request%20for%20Postgres%20upgrade%20for%20project'
|
||||
const message = `Upgrade information:%0A• Manual intervention reason: ${requires_manual_intervention}`
|
||||
|
||||
@@ -60,19 +62,30 @@ const InfrastructureInfo = () => {
|
||||
</div>
|
||||
</div>
|
||||
</ScaffoldContainer>
|
||||
|
||||
<ScaffoldDivider />
|
||||
|
||||
{readReplicasEnabled && (
|
||||
<>
|
||||
<InstanceConfiguration />
|
||||
<ScaffoldDivider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<ScaffoldContainer>
|
||||
{!readReplicasEnabled && (
|
||||
<ScaffoldSection>
|
||||
<ScaffoldSectionDetail>
|
||||
<p>Configuration</p>
|
||||
<p className="text-foreground-light text-sm">Information on your server provider</p>
|
||||
</ScaffoldSectionDetail>
|
||||
<ScaffoldSectionContent>
|
||||
<Input readOnly disabled value={project?.cloud_provider} label="Cloud provider" />
|
||||
<Input readOnly disabled value={project?.region} label="Region" />
|
||||
</ScaffoldSectionContent>
|
||||
</ScaffoldSection>
|
||||
)}
|
||||
<ScaffoldSection>
|
||||
<ScaffoldSectionDetail>
|
||||
<p>Configuration</p>
|
||||
<p className="text-foreground-light text-sm">Information on your server provider</p>
|
||||
</ScaffoldSectionDetail>
|
||||
<ScaffoldSectionContent>
|
||||
<Input readOnly disabled value={project?.cloud_provider} label="Cloud provider" />
|
||||
<Input readOnly disabled value={project?.region} label="Region" />
|
||||
</ScaffoldSectionContent>
|
||||
</ScaffoldSection>
|
||||
<ScaffoldSection className="!pt-0">
|
||||
<ScaffoldSectionDetail>
|
||||
<p>Service Versions</p>
|
||||
<p className="text-foreground-light text-sm">
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { MOCK_DATABASES } from 'components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Popover_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Button,
|
||||
IconChevronDown,
|
||||
PopoverContent_Shadcn_,
|
||||
Command_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
ScrollArea,
|
||||
CommandItem_Shadcn_,
|
||||
IconCheck,
|
||||
} from 'ui'
|
||||
|
||||
interface DatabaseSelectorProps {
|
||||
selectedDatabaseId: string
|
||||
onChangeDatabaseId: (id: string) => void
|
||||
}
|
||||
|
||||
const DatabaseSelector = ({ selectedDatabaseId, onChangeDatabaseId }: DatabaseSelectorProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const databases = MOCK_DATABASES.sort((a, b) => (a.id > b.id ? 1 : -1))
|
||||
const selectedDatabase = databases.find((db) => db.id.toString() === selectedDatabaseId)
|
||||
|
||||
return (
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<div className="flex items-center space-x-2 cursor-pointer">
|
||||
<Button
|
||||
type="default"
|
||||
className="pr-2"
|
||||
iconRight={
|
||||
<IconChevronDown className="text-foreground-light" strokeWidth={2} size={12} />
|
||||
}
|
||||
>
|
||||
Source:{' '}
|
||||
<span className="capitalize">
|
||||
{(selectedDatabase?.type ?? '').split('_').join(' ').toLowerCase()}
|
||||
{selectedDatabase?.type === 'PRIMARY' && ' database'}
|
||||
</span>{' '}
|
||||
{selectedDatabase?.type === 'READ_REPLICA' && <span>(ID: {selectedDatabase?.id})</span>}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_ className="p-0 w-48" side="bottom" align="end">
|
||||
<Command_Shadcn_>
|
||||
<CommandList_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
<ScrollArea className={(databases || []).length > 7 ? 'h-[210px]' : ''}>
|
||||
{databases?.map((database) => {
|
||||
return (
|
||||
<CommandItem_Shadcn_
|
||||
key={database.id}
|
||||
value={database.id.toString()}
|
||||
className="cursor-pointer w-full"
|
||||
onSelect={() => {
|
||||
onChangeDatabaseId(database.id.toString())
|
||||
setOpen(false)
|
||||
}}
|
||||
onClick={() => {
|
||||
onChangeDatabaseId(database.id.toString())
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<p>
|
||||
{database.type === 'PRIMARY'
|
||||
? 'Primary database'
|
||||
: `Read replica (ID: ${database.id})`}
|
||||
</p>
|
||||
{database.id.toString() === selectedDatabaseId && <IconCheck />}
|
||||
</div>
|
||||
</CommandItem_Shadcn_>
|
||||
)
|
||||
})}
|
||||
</ScrollArea>
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
export default DatabaseSelector
|
||||
@@ -3,6 +3,7 @@ import { get } from 'lib/common/fetch'
|
||||
import { API_URL, DEFAULT_PROJECT_API_SERVICE_ID } from 'lib/constants'
|
||||
import { useCallback } from 'react'
|
||||
import { configKeys } from './keys'
|
||||
import { ResponseError } from 'types'
|
||||
|
||||
export type ProjectSettingsVariables = {
|
||||
projectRef?: string
|
||||
@@ -77,7 +78,7 @@ export async function getProjectSettings(
|
||||
}
|
||||
|
||||
export type ProjectSettingsData = Awaited<ReturnType<typeof getProjectSettings>>
|
||||
export type ProjectSettingsError = unknown
|
||||
export type ProjectSettingsError = ResponseError
|
||||
|
||||
export const useProjectSettingsQuery = <TData = ProjectSettingsData>(
|
||||
{ projectRef }: ProjectSettingsVariables,
|
||||
|
||||
@@ -22,6 +22,8 @@ export const AWS_REGIONS = {
|
||||
// SOUTH_AFRICA: 'South Africa (Cape Town)',
|
||||
} as const
|
||||
|
||||
export type AWS_REGIONS_KEYS = keyof typeof AWS_REGIONS
|
||||
|
||||
export const FLY_REGIONS = {
|
||||
SOUTHEAST_ASIA: 'Singapore',
|
||||
} as const
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-inlinesvg": "^4.0.4",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-simple-maps": "^3.0.0",
|
||||
"react-split": "^2.0.13",
|
||||
"react-tracked": "^1.7.11",
|
||||
"react-virtualized-auto-sizer": "^1.0.20",
|
||||
@@ -127,6 +128,7 @@
|
||||
"@types/react-datepicker": "^4.3.4",
|
||||
"@types/react-dom": "^18.2.8",
|
||||
"@types/react-grid-layout": "^1.3.0",
|
||||
"@types/react-simple-maps": "^3.0.1",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/react-window-infinite-loader": "^1.0.5",
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'styles/storage.scss'
|
||||
import 'styles/stripe.scss'
|
||||
import 'styles/toast.scss'
|
||||
import 'styles/ui.scss'
|
||||
import 'styles/reactflow.scss'
|
||||
|
||||
import { loader } from '@monaco-editor/react'
|
||||
import { TooltipProvider } from '@radix-ui/react-tooltip'
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
.instance-configuration {
|
||||
.react-flow__pane {
|
||||
@apply cursor-default;
|
||||
}
|
||||
.react-flow__node {
|
||||
@apply cursor-default;
|
||||
}
|
||||
}
|
||||
|
||||
.map-path {
|
||||
animation: dash 8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
to {
|
||||
stroke-dashoffset: 100;
|
||||
}
|
||||
}
|
||||
Generated
+178
@@ -808,6 +808,7 @@
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-inlinesvg": "^4.0.4",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-simple-maps": "^3.0.0",
|
||||
"react-split": "^2.0.13",
|
||||
"react-tracked": "^1.7.11",
|
||||
"react-virtualized-auto-sizer": "^1.0.20",
|
||||
@@ -851,6 +852,7 @@
|
||||
"@types/react-datepicker": "^4.3.4",
|
||||
"@types/react-dom": "^18.2.8",
|
||||
"@types/react-grid-layout": "^1.3.0",
|
||||
"@types/react-simple-maps": "^3.0.1",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/react-window-infinite-loader": "^1.0.5",
|
||||
@@ -11235,6 +11237,58 @@
|
||||
"redux": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-simple-maps": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-simple-maps/-/react-simple-maps-3.0.4.tgz",
|
||||
"integrity": "sha512-U9qnX0wVhxldrTpsase44fIoLpyO1OT/hgNMRoJTixj1qjpMRdSRIfih93mR3D/Tss/8CmM7dPwKMjtaGkDpmw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/d3-geo": "^2",
|
||||
"@types/d3-zoom": "^2",
|
||||
"@types/geojson": "*",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-simple-maps/node_modules/@types/d3-color": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.6.tgz",
|
||||
"integrity": "sha512-tbaFGDmJWHqnenvk3QGSvD3RVwr631BjKRD7Sc7VLRgrdX5mk5hTyoeBL6rXZaeoXzmZwIl1D2HPogEdt1rHBg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/react-simple-maps/node_modules/@types/d3-geo": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-2.0.7.tgz",
|
||||
"integrity": "sha512-RIXlxPdxvX+LAZFv+t78CuYpxYag4zuw9mZc+AwfB8tZpKU90rMEn2il2ADncmeZlb7nER9dDsJpRisA3lRvjA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-simple-maps/node_modules/@types/d3-interpolate": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.5.tgz",
|
||||
"integrity": "sha512-UINE41RDaUMbulp+bxQMDnhOi51rh5lA2dG+dWZU0UY/IwQiG/u2x8TfnWYU9+xwGdXsJoAvrBYUEQl0r91atg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/d3-color": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-simple-maps/node_modules/@types/d3-selection": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-2.0.4.tgz",
|
||||
"integrity": "sha512-5a21DF7avVPmiUau8KTsv5r76yGqbMgq4QtByoCBPXUrVFWFkd3Ob4OOhmePNRbQqfUCNFjgB4sO7sUURnKcBg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/react-simple-maps/node_modules/@types/d3-zoom": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-2.0.7.tgz",
|
||||
"integrity": "sha512-JWke4E8ZyrKUQ68ESTWSK16fVb0OYnaiJ+WXJRYxKLn4aXU0o4CLYxMWBEiouUfO3TTCoyroOrGPcBG6u1aAxA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/d3-interpolate": "^2",
|
||||
"@types/d3-selection": "^2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-syntax-highlighter": {
|
||||
"version": "15.5.7",
|
||||
"dev": true,
|
||||
@@ -29746,6 +29800,112 @@
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-simple-maps": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-simple-maps/-/react-simple-maps-3.0.0.tgz",
|
||||
"integrity": "sha512-vKNFrvpPG8Vyfdjnz5Ne1N56rZlDfHXv5THNXOVZMqbX1rWZA48zQuYT03mx6PAKanqarJu/PDLgshIZAfHHqw==",
|
||||
"dependencies": {
|
||||
"d3-geo": "^2.0.2",
|
||||
"d3-selection": "^2.0.0",
|
||||
"d3-zoom": "^2.0.0",
|
||||
"topojson-client": "^3.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.8.0 || 17.x || 18.x",
|
||||
"react-dom": "^16.8.0 || 17.x || 18.x"
|
||||
}
|
||||
},
|
||||
"node_modules/react-simple-maps/node_modules/d3-array": {
|
||||
"version": "2.12.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
|
||||
"integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
|
||||
"dependencies": {
|
||||
"internmap": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-simple-maps/node_modules/d3-color": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-2.0.0.tgz",
|
||||
"integrity": "sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ=="
|
||||
},
|
||||
"node_modules/react-simple-maps/node_modules/d3-dispatch": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz",
|
||||
"integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA=="
|
||||
},
|
||||
"node_modules/react-simple-maps/node_modules/d3-drag": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-2.0.0.tgz",
|
||||
"integrity": "sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w==",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 2",
|
||||
"d3-selection": "2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-simple-maps/node_modules/d3-ease": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-2.0.0.tgz",
|
||||
"integrity": "sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ=="
|
||||
},
|
||||
"node_modules/react-simple-maps/node_modules/d3-geo": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.2.tgz",
|
||||
"integrity": "sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==",
|
||||
"dependencies": {
|
||||
"d3-array": "^2.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-simple-maps/node_modules/d3-interpolate": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-2.0.1.tgz",
|
||||
"integrity": "sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-simple-maps/node_modules/d3-selection": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-2.0.0.tgz",
|
||||
"integrity": "sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA=="
|
||||
},
|
||||
"node_modules/react-simple-maps/node_modules/d3-timer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz",
|
||||
"integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA=="
|
||||
},
|
||||
"node_modules/react-simple-maps/node_modules/d3-transition": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-2.0.0.tgz",
|
||||
"integrity": "sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog==",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 2",
|
||||
"d3-dispatch": "1 - 2",
|
||||
"d3-ease": "1 - 2",
|
||||
"d3-interpolate": "1 - 2",
|
||||
"d3-timer": "1 - 2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"d3-selection": "2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-simple-maps/node_modules/d3-zoom": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-2.0.0.tgz",
|
||||
"integrity": "sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 2",
|
||||
"d3-drag": "2",
|
||||
"d3-interpolate": "1 - 2",
|
||||
"d3-selection": "2",
|
||||
"d3-transition": "2"
|
||||
}
|
||||
},
|
||||
"node_modules/react-simple-maps/node_modules/internmap": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
|
||||
"integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="
|
||||
},
|
||||
"node_modules/react-smooth": {
|
||||
"version": "2.0.4",
|
||||
"license": "MIT",
|
||||
@@ -33515,6 +33675,24 @@
|
||||
"version": "2.3.6",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/topojson-client": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
|
||||
"integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
|
||||
"dependencies": {
|
||||
"commander": "2"
|
||||
},
|
||||
"bin": {
|
||||
"topo2geo": "bin/topo2geo",
|
||||
"topomerge": "bin/topomerge",
|
||||
"topoquantize": "bin/topoquantize"
|
||||
}
|
||||
},
|
||||
"node_modules/topojson-client/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/toposort": {
|
||||
"version": "2.0.2",
|
||||
"license": "MIT"
|
||||
|
||||
Reference in new issue
Block a user