-
- {secret.updated_at === secret.created_at ? 'Added' : 'Updated'} on{' '}
- {dayjs(secret.updated_at).format('MMM D, YYYY')}
-
-
+ if (col.id === 'actions') {
+ return (
+
e.stopPropagation()}>
} />
-
+
{
onSelectRemove(secret)}
+ onClick={() => onSelectRemove(row)}
tooltip={{
content: {
side: 'left',
@@ -134,8 +90,71 @@ const SecretRow = ({ secret, onSelectRemove }: SecretRowProps) => {
-
+
+ )
+ }
+
+ if (col.id === 'secret_value') {
+ return (
+
e.stopPropagation()}>
+
+ ) : !revealSecret ? (
+
+ ) : (
+
+ )
+ }
+ onClick={() => setRevealSecret(!revealSecret)}
+ />
+
+ {revealSecret && revealedValue !== undefined ? (
+
+ ) : (
+
••••••••••••••••••
+ )}
+
+
+ )
+ }
+
+ if (col.id === 'updated_at') {
+ return (
+
+
+ {row.updated_at === row.created_at ? 'Added' : 'Updated'} on{' '}
+ {dayjs(row.updated_at).format('MMM D, YYYY')}
+
+
+ )
+ }
+
+ if (col.id === 'id') {
+ return (
+
+ )
+ }
+
+ return (
+
+
+ {name}
+
+ {row.description !== undefined && row.description !== '' && (
+
+ )}
)
}
diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.utils.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.utils.tsx
new file mode 100644
index 0000000000000..a1bec3a55dbc1
--- /dev/null
+++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/Secrets.utils.tsx
@@ -0,0 +1,57 @@
+import type { Column } from 'react-data-grid'
+
+import type { VaultSecret } from 'types'
+import { cn } from 'ui'
+import SecretRow from './SecretRow'
+
+export type SecretColumnId = 'secret' | 'id' | 'secret_value' | 'updated_at' | 'actions'
+
+export interface SecretTableColumn {
+ id: SecretColumnId
+ name: string
+ minWidth?: number
+ width?: number
+ maxWidth?: number
+}
+
+export const SECRET_TABLE_COLUMNS: SecretTableColumn[] = [
+ { id: 'secret', name: 'Secret', minWidth: 300, width: 360 },
+ { id: 'id', name: 'ID', minWidth: 220, width: 260 },
+ { id: 'secret_value', name: 'Value', minWidth: 320, width: 420 },
+ { id: 'updated_at', name: 'Last updated', minWidth: 180 },
+ { id: 'actions', name: '', minWidth: 75, width: 75 },
+]
+
+export const formatSecretColumns = ({
+ onSelectRemove,
+}: {
+ onSelectRemove: (secret: VaultSecret) => void
+}): Column
[] => {
+ return SECRET_TABLE_COLUMNS.map((col) => {
+ const result: Column = {
+ key: col.id,
+ name: col.name,
+ minWidth: col.minWidth ?? 100,
+ maxWidth: col.maxWidth,
+ width: col.width,
+ resizable: false,
+ sortable: false,
+ draggable: false,
+ headerCellClass: undefined,
+ renderHeaderCell: () => {
+ return (
+
+ )
+ },
+ renderCell: ({ row }) => ,
+ }
+ return result
+ })
+}
diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx
index e1278afbbb8a1..05bd8a8f7264f 100644
--- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx
+++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx
@@ -1,7 +1,8 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
+import DataGrid, { Row } from 'react-data-grid'
import { sortBy } from 'lodash'
-import { Loader, Search, X } from 'lucide-react'
-import { Fragment, useEffect, useState } from 'react'
+import { Loader, RefreshCw, Search, X } from 'lucide-react'
+import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
@@ -13,16 +14,17 @@ import type { VaultSecret } from 'types'
import {
Button,
Input,
+ LoadingLine,
+ cn,
Select_Shadcn_,
- SelectContent_Shadcn_,
- SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
- Separator,
+ SelectContent_Shadcn_,
+ SelectItem_Shadcn_,
} from 'ui'
import AddNewSecretModal from './AddNewSecretModal'
import DeleteSecretModal from './DeleteSecretModal'
-import SecretRow from './SecretRow'
+import { formatSecretColumns } from './Secrets.utils'
export const SecretsManagement = () => {
const { search } = useParams()
@@ -31,140 +33,162 @@ export const SecretsManagement = () => {
const [searchValue, setSearchValue] = useState('')
const [showAddSecretModal, setShowAddSecretModal] = useState(false)
const [selectedSecretToRemove, setSelectedSecretToRemove] = useState()
- const [selectedSort, setSelectedSort] = useState('updated_at')
+ const [selectedSort, setSelectedSort] = useState<'updated_at' | 'name'>('updated_at')
const { can: canManageSecrets } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'tables'
)
- const { data, isLoading } = useVaultSecretsQuery({
+ const { data, isLoading, isRefetching, refetch, error, isError } = useVaultSecretsQuery({
projectRef: project?.ref!,
connectionString: project?.connectionString,
})
- const allSecrets = data || []
- const secrets = sortBy(
- searchValue.length > 0
- ? allSecrets.filter(
- (secret) =>
- (secret?.name ?? '').toLowerCase().includes(searchValue.toLowerCase()) ||
- (secret?.id ?? '').toLowerCase().includes(searchValue.toLowerCase())
- )
- : allSecrets,
- (s) => {
- if (selectedSort === 'updated_at') {
- return Number(new Date(s.updated_at))
- } else {
- return s[selectedSort as keyof VaultSecret]
- }
+ const allSecrets = useMemo(() => data || [], [data])
+ const secrets = useMemo(() => {
+ const filtered =
+ searchValue.length > 0
+ ? allSecrets.filter(
+ (secret) =>
+ (secret?.name ?? '').toLowerCase().includes(searchValue.trim().toLowerCase()) ||
+ (secret?.id ?? '').toLowerCase().includes(searchValue.trim().toLowerCase())
+ )
+ : allSecrets
+
+ if (selectedSort === 'updated_at') {
+ return sortBy(filtered, (s) => Number(new Date(s.updated_at))).reverse()
}
- )
+ return sortBy(filtered, (s) => (s.name || '').toLowerCase())
+ }, [allSecrets, searchValue, selectedSort])
useEffect(() => {
if (search !== undefined) setSearchValue(search)
}, [search])
+ const columns = useMemo(
+ () =>
+ formatSecretColumns({
+ onSelectRemove: (secret) => setSelectedSecretToRemove(secret),
+ }),
+ []
+ )
+
return (
<>
-
-
-
- setSearchValue(event.target.value)}
- icon={ }
- actions={
- searchValue.length > 0
- ? [
- }
- className="px-1"
- onClick={() => setSearchValue('')}
- />,
- ]
- : []
- }
- />
-
-
-
- <>Sort by {selectedSort}>
-
-
-
-
- Updated at
-
-
- Name
-
-
-
-
-
-
-
setShowAddSecretModal(true)}
- tooltip={{
- content: {
- side: 'bottom',
- text: !canManageSecrets
- ? 'You need additional permissions to add secrets'
- : undefined,
- },
- }}
- >
- Add new secret
-
+
+
+
+
+ }
+ value={searchValue ?? ''}
+ onChange={(e) => setSearchValue(e.target.value)}
+ actions={[
+ searchValue && (
+ }
+ onClick={() => {
+ setSearchValue('')
+ }}
+ className="p-0 h-5 w-5"
+ />
+ ),
+ ]}
+ />
+
+ setSelectedSort(v as any)}>
+
+
+ <>Sort by {selectedSort}>
+
+
+
+
+ Updated at
+
+
+ Name
+
+
+
+
+
+
+ }
+ loading={isRefetching}
+ onClick={() => refetch()}
+ >
+ Refresh
+
+
+ setShowAddSecretModal(true)}
+ tooltip={{
+ content: {
+ side: 'bottom',
+ text: !canManageSecrets
+ ? 'You need additional permissions to add secrets'
+ : undefined,
+ },
+ }}
+ >
+ Add new secret
+
+
-
- {/* Table of secrets */}
-
- {isLoading ? (
+
+
+ {isError ? (
-
-
Loading secrets from the Vault
+
Failed to load secrets
) : (
- <>
- {secrets.map((secret, idx) => {
- return (
-
-
- {idx !== secrets.length - 1 && }
-
+
row.id}
+ rowClass={() => {
+ return cn(
+ 'cursor-pointer',
+ '[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none',
+ '[&>.rdg-cell:first-child>div]:pl-8'
)
- })}
- {secrets.length === 0 && (
- <>
- {searchValue.length === 0 ? (
-
-
No secrets added yet
-
- The Vault allows you to store sensitive information like API keys
-
-
- ) : (
-
-
No results found
-
- Your search for "{searchValue}" did not return any results
-
-
- )}
- >
- )}
- >
+ }}
+ renderers={{
+ renderRow(_, props) {
+ return
+ },
+ }}
+ />
)}
+
+ {secrets.length === 0 && !isLoading && !isError ? (
+
+
+
+ {searchValue ? 'No secrets found' : 'No secrets added yet'}
+
+
+ {searchValue
+ ? `There are currently no secrets based on the search "${searchValue}"`
+ : 'The Vault allows you to store sensitive information like API keys'}
+
+
+
+ ) : null}
diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx
index ea047d76d18d2..ce683309f802e 100644
--- a/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx
+++ b/apps/studio/components/interfaces/Organization/TeamSettings/MemberRow.tsx
@@ -1,7 +1,6 @@
import { ArrowRight, Check, Minus, User, X } from 'lucide-react'
import Link from 'next/link'
-import Table from 'components/to-be-cleaned/Table'
import PartnerIcon from 'components/ui/PartnerIcon'
import { ProfileImage } from 'components/ui/ProfileImage'
import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query'
@@ -17,6 +16,8 @@ import {
HoverCard_Shadcn_,
ScrollArea,
cn,
+ TableRow,
+ TableCell,
} from 'ui'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { isInviteExpired } from '../Organization.utils'
@@ -68,8 +69,8 @@ export const MemberRow = ({ member }: MemberRowProps) => {
}).length > 0
return (
-
-
+
+
-
+
-
+
{isInvitedUser && member.invited_at && (
{isInviteExpired(member.invited_at) ? 'Expired' : 'Invited'}
)}
{member.is_sso_user && SSO }
-
+
-
+
{member.mfa_enabled ? (
@@ -122,9 +123,9 @@ export const MemberRow = ({ member }: MemberRowProps) => {
)}
-
+
-
+
{isLoadingRoles ? (
) : isObfuscated ? (
@@ -201,11 +202,11 @@ export const MemberRow = ({ member }: MemberRowProps) => {
)
})
)}
-
+
-
+
-
-
+
+
)
}
diff --git a/apps/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx b/apps/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx
index 3ecc3b3de9041..22b3d06998234 100644
--- a/apps/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx
+++ b/apps/studio/components/interfaces/Organization/TeamSettings/MembersView.tsx
@@ -1,7 +1,6 @@
import { AlertCircle, HelpCircle } from 'lucide-react'
import { useParams } from 'common'
-import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query'
@@ -9,7 +8,20 @@ import { useOrganizationMembersQuery } from 'data/organizations/organization-mem
import { useProfile } from 'lib/profile'
import { partition } from 'lodash'
import { useMemo } from 'react'
-import { Button, Loading, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
+import {
+ Button,
+ Loading,
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+ Table,
+ TableHeader,
+ TableHead,
+ TableBody,
+ TableCell,
+ TableRow,
+ Card,
+} from 'ui'
import { Admonition } from 'ui-patterns'
import { MemberRow } from './MemberRow'
@@ -78,77 +90,86 @@ const MembersView = ({ searchString }: MembersViewProps) => {
{isSuccessMembers && (
-
- User,
- ,
-
- Enabled MFA
- ,
-
- Role
-
-
-
-
-
-
-
-
- How to configure access control?
-
- ,
- ,
- ]}
- body={[
- ...(isSuccessRoles && isSuccessMembers && !isOrgScopedRole
- ? [
-
-
-
-
- ,
- ]
- : []),
- ...(!!user ? [ ] : []),
- ...sortedMembers.map((member) => (
-
- )),
- ...(searchString.length > 0 && filteredMembers.length === 0
- ? [
-
-
-
-
-
- No users matched the search query "{searchString}"
-
-
-
- ,
- ]
- : []),
-
-
-
- {searchString ? `${filteredMembers.length} of ` : ''}
- {members.length || '0'} {members.length == 1 ? 'user' : 'users'}
-
-
- ,
- ]}
- />
-
+
+
+
+
+
+ User
+
+
+ Enabled MFA
+
+
+ Role
+
+
+
+
+
+
+
+
+
+ How to configure access control?
+
+
+
+
+
+
+
+
+ {[
+ ...(isSuccessRoles && isSuccessMembers && !isOrgScopedRole
+ ? [
+
+
+
+
+ ,
+ ]
+ : []),
+ ...(!!user ? [ ] : []),
+ ...sortedMembers.map((member) => (
+
+ )),
+ ...(searchString.length > 0 && filteredMembers.length === 0
+ ? [
+
+
+
+
+
+ No users matched the search query "{searchString}"
+
+
+
+ ,
+ ]
+ : []),
+
+
+
+ {searchString ? `${filteredMembers.length} of ` : ''}
+ {members.length || '0'} {members.length == 1 ? 'user' : 'users'}
+
+
+ ,
+ ]}
+
+
+
+
)}
>
diff --git a/apps/studio/pages/project/[ref]/database/extensions.tsx b/apps/studio/pages/project/[ref]/database/extensions.tsx
index fd663c2a53f34..4239872f36ade 100644
--- a/apps/studio/pages/project/[ref]/database/extensions.tsx
+++ b/apps/studio/pages/project/[ref]/database/extensions.tsx
@@ -4,7 +4,7 @@ import { Extensions } from 'components/interfaces/Database'
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
-import { FormHeader } from 'components/ui/Forms/FormHeader'
+import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import NoPermission from 'components/ui/NoPermission'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import type { NextPageWithLayout } from 'types'
@@ -18,14 +18,17 @@ const DatabaseExtensions: NextPageWithLayout = () => {
}
return (
-
-
-
-
-
+
+
+
)
}
diff --git a/apps/studio/styles/typography.scss b/apps/studio/styles/typography.scss
index 453f3df940405..b284618ace54e 100644
--- a/apps/studio/styles/typography.scss
+++ b/apps/studio/styles/typography.scss
@@ -35,32 +35,32 @@
@layer utilities {
/* Heading */
.heading-title {
- @apply scroll-m-20 text-2xl tracking-tight text-foreground;
+ @apply scroll-m-20 text-2xl tracking-tight;
}
.heading-section {
- @apply scroll-m-20 text-xl text-foreground;
+ @apply scroll-m-20 text-xl;
}
.heading-subSection {
- @apply scroll-m-20 text-base text-foreground;
+ @apply scroll-m-20 text-base;
}
.heading-default {
- @apply scroll-m-20 text-sm font-medium text-foreground;
+ @apply scroll-m-20 text-sm font-medium;
}
.heading-compact {
- @apply scroll-m-20 text-xs font-medium text-foreground;
+ @apply scroll-m-20 text-xs font-medium;
}
.heading-meta {
- @apply text-xs font-mono uppercase tracking-wider font-medium text-foreground-light;
+ @apply text-xs font-mono uppercase tracking-wider font-medium;
}
/* Text */
.text-default {
- @apply text-base text-foreground-light;
+ @apply text-base;
}
.text-subTitle {
diff --git a/packages/ui/src/components/shadcn/ui/card.tsx b/packages/ui/src/components/shadcn/ui/card.tsx
index 59529534d41f8..e72a26a4066ad 100644
--- a/packages/ui/src/components/shadcn/ui/card.tsx
+++ b/packages/ui/src/components/shadcn/ui/card.tsx
@@ -6,7 +6,10 @@ const Card = React.forwardRef (
)
diff --git a/packages/ui/src/components/shadcn/ui/table.tsx b/packages/ui/src/components/shadcn/ui/table.tsx
index 186fadfe9e6ff..ef716e89d09ea 100644
--- a/packages/ui/src/components/shadcn/ui/table.tsx
+++ b/packages/ui/src/components/shadcn/ui/table.tsx
@@ -19,7 +19,7 @@ const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes
>(({ className, ...props }, ref) => (
-
+ tr]:bg-200', className)} {...props} />
))
TableHeader.displayName = 'TableHeader'
@@ -64,7 +64,7 @@ const TableHead = React.forwardRef<