Skip to content

Commit d10001b

Browse files
SaxonFCopilotcharislamjoshenlim
authored
Advisor sidebar manager (supabase#39889)
* sidebar-manager * storage keys * tests * more ai spots * test fix * revert to default * remove ref * Update apps/studio/state/sidebar-manager-state.tsx Co-authored-by: Copilot <[email protected]> * Update apps/studio/components/ui/AIAssistantPanel/AIAssistant.tsx Co-authored-by: Copilot <[email protected]> * fix ts * fix * fux * fux query param * clean * fix * more * mock local storage * simplify * remove provider test * remve useopensidebar * fix(new homepage): open ai assistant on advisor card button clicks * Update apps/studio/components/layouts/ProjectLayout/LayoutSidebar/index.tsx Co-authored-by: Charis <[email protected]> * Update apps/studio/state/sidebar-manager-state.tsx Co-authored-by: Charis <[email protected]> * refine * editor sidebar manager * reset results * advisor sidebar manager * empty state and notice * event tracking * remove variable * remove use effect * open in sidebar * use sidebar old home * Update apps/studio/components/ui/EditorPanel/EditorPanel.tsx Co-authored-by: Charis <[email protected]> * connect hotkey * Update apps/studio/components/layouts/AppLayout/AssistantButton.tsx Co-authored-by: Charis <[email protected]> * Update apps/studio/state/advisor-state.ts Co-authored-by: Charis <[email protected]> * Update apps/studio/state/advisor-state.ts Co-authored-by: Charis <[email protected]> * fix * initial prompt * fix(inline editor button): only show keyboard shortcut if hotkey active * cleanup(advisor panel): minor code cleanup * fix(advisor panel): misplaced key on list * fix(advisor panel): add error state * fix(advisor panel): improve a11y * fix(advisor panel): cannot find selected item * fix * fix * tooltip * link * sidebar move up * LayoutSidebarProvider to only sendEvent if in a project --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Charis Lam <[email protected]> Co-authored-by: Joshen Lim <[email protected]>
1 parent faf8107 commit d10001b

File tree

23 files changed

+733
-171
lines changed

23 files changed

+733
-171
lines changed

apps/studio/components/interfaces/Home/AdvisorWidget.tsx

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import { Activity, ExternalLink, Shield } from 'lucide-react'
22
import Link from 'next/link'
3-
import { useMemo, useState } from 'react'
3+
import { useCallback, useMemo, useState } from 'react'
44

55
import { useParams } from 'common'
66
import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
7-
import { EntityTypeIcon, lintInfoMap } from 'components/interfaces/Linter/Linter.utils'
7+
import { EntityTypeIcon, createLintSummaryPrompt } from 'components/interfaces/Linter/Linter.utils'
88
import { useQueryPerformanceQuery } from 'components/interfaces/Reports/Reports.queries'
99
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
1010
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
11+
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
12+
import { useAdvisorStateSnapshot } from 'state/advisor-state'
1113
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
14+
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
1215
import {
1316
AiIconAnimation,
1417
Card,
@@ -27,8 +30,6 @@ import {
2730
TabsTrigger_Shadcn_ as TabsTrigger,
2831
} from 'ui'
2932
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
30-
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
31-
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
3233

3334
interface SlowQuery {
3435
rolname: string
@@ -48,6 +49,7 @@ export const AdvisorWidget = () => {
4849
)
4950
const snap = useAiAssistantStateSnapshot()
5051
const { openSidebar } = useSidebarManagerSnapshot()
52+
const { setSelectedItemId } = useAdvisorStateSnapshot()
5153

5254
const securityLints = useMemo(
5355
() => (lints ?? []).filter((lint: Lint) => lint.categories.includes('SECURITY')),
@@ -76,6 +78,14 @@ export const AdvisorWidget = () => {
7678
[slowestQueriesData]
7779
)
7880

81+
const handleLintClick = useCallback(
82+
(lint: Lint) => {
83+
setSelectedItemId(lint.cache_key)
84+
openSidebar(SIDEBAR_KEYS.ADVISOR_PANEL)
85+
},
86+
[setSelectedItemId, openSidebar]
87+
)
88+
7989
const totalIssues =
8090
securityErrorCount + securityWarningCount + performanceErrorCount + performanceWarningCount
8191
const hasErrors = securityErrorCount > 0 || performanceErrorCount > 0
@@ -134,15 +144,15 @@ export const AdvisorWidget = () => {
134144
className="text-sm w-full border-b my-0 last:border-b-0 group px-4 "
135145
>
136146
<div className="flex items-center justify-between w-full group">
137-
<Link
138-
href={`/project/${projectRef}/advisors/${title.toLowerCase()}?id=${lint.cache_key}&preset=${lint.level}`}
139-
className="flex items-center gap-2 transition truncate flex-1 min-w-0 py-3"
147+
<button
148+
onClick={() => handleLintClick(lint)}
149+
className="flex items-center gap-2 transition truncate flex-1 min-w-0 py-3 text-left"
140150
>
141151
<EntityTypeIcon type={lint.metadata?.type} />
142152
<p className="flex-1 font-mono text-xs leading-6 text-xs text-foreground-light group-hover:text-foreground truncate">
143153
{lintText.replace(/\\`/g, '`')}
144154
</p>
145-
</Link>
155+
</button>
146156
<ButtonTooltip
147157
type="text"
148158
className="px-1 opacity-0 group-hover:opacity-100 w-7"
@@ -153,16 +163,11 @@ export const AdvisorWidget = () => {
153163
openSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
154164
snap.newChat({
155165
name: 'Summarize lint',
156-
initialInput: `Summarize the issue and suggest fixes for the following lint item:
157-
Title: ${lintInfoMap.find((item) => item.name === lint.name)?.title ?? lint.title}
158-
Entity: ${(lint.metadata && (lint.metadata.entity || (lint.metadata.schema && lint.metadata.name && `${lint.metadata.schema}.${lint.metadata.name}`))) ?? 'N/A'}
159-
Schema: ${lint.metadata?.schema ?? 'N/A'}
160-
Issue Details: ${lint.detail ? lint.detail.replace(/\`/g, '`') : 'N/A'}
161-
Description: ${lint.description ? lint.description.replace(/\`/g, '`') : 'N/A'}`,
166+
initialInput: createLintSummaryPrompt(lint),
162167
})
163168
}}
164169
tooltip={{
165-
content: { side: 'bottom', text: 'What is this issue?' },
170+
content: { side: 'bottom', text: 'Help me fix this issue' },
166171
}}
167172
/>
168173
</div>

apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx

Lines changed: 8 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,18 @@
11
import { BarChart, Shield } from 'lucide-react'
2-
import { useCallback, useMemo, useState } from 'react'
2+
import { useCallback, useMemo } from 'react'
33

44
import { useParams } from 'common'
5-
import LintDetail from 'components/interfaces/Linter/LintDetail'
65
import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants'
7-
import {
8-
createLintSummaryPrompt,
9-
LintCategoryBadge,
10-
lintInfoMap,
11-
} from 'components/interfaces/Linter/Linter.utils'
6+
import { createLintSummaryPrompt } from 'components/interfaces/Linter/Linter.utils'
127
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
138
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
149
import { Lint, useProjectLintsQuery } from 'data/lint/lint-query'
1510
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
1611
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
12+
import { useAdvisorStateSnapshot } from 'state/advisor-state'
1713
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
1814
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
19-
import {
20-
AiIconAnimation,
21-
Button,
22-
Card,
23-
CardContent,
24-
CardHeader,
25-
CardTitle,
26-
Sheet,
27-
SheetContent,
28-
SheetHeader,
29-
SheetSection,
30-
SheetTitle,
31-
} from 'ui'
15+
import { AiIconAnimation, Button, Card, CardContent, CardHeader, CardTitle } from 'ui'
3216
import { Row } from 'ui-patterns'
3317
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
3418

@@ -46,8 +30,7 @@ export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: bo
4630
const { mutate: sendEvent } = useSendEventMutation()
4731
const { data: organization } = useSelectedOrganizationQuery()
4832
const { openSidebar } = useSidebarManagerSnapshot()
49-
50-
const [selectedLint, setSelectedLint] = useState<Lint | null>(null)
33+
const { setSelectedItemId } = useAdvisorStateSnapshot()
5134

5235
const errorLints: Lint[] = useMemo(() => {
5336
return lints?.filter((lint) => lint.level === LINTER_LEVELS.ERROR) ?? []
@@ -84,7 +67,8 @@ export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: bo
8467

8568
const handleCardClick = useCallback(
8669
(lint: Lint) => {
87-
setSelectedLint(lint)
70+
setSelectedItemId(lint.cache_key)
71+
openSidebar(SIDEBAR_KEYS.ADVISOR_PANEL)
8872
if (projectRef && organization?.slug) {
8973
sendEvent({
9074
action: 'home_advisor_issue_card_clicked',
@@ -100,7 +84,7 @@ export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: bo
10084
})
10185
}
10286
},
103-
[sendEvent, projectRef, organization]
87+
[sendEvent, setSelectedItemId, openSidebar, projectRef, organization, totalErrors]
10488
)
10589

10690
if (showEmptyState) {
@@ -185,32 +169,6 @@ export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: bo
185169
)
186170
})}
187171
</Row>
188-
<Sheet open={selectedLint !== null} onOpenChange={() => setSelectedLint(null)}>
189-
<SheetContent>
190-
{selectedLint && (
191-
<>
192-
<SheetHeader>
193-
<div className="flex items-center gap-4">
194-
<SheetTitle>
195-
{lintInfoMap.find((item) => item.name === selectedLint.name)?.title ??
196-
'Unknown'}
197-
</SheetTitle>
198-
<LintCategoryBadge category={selectedLint.categories[0]} />
199-
</div>
200-
</SheetHeader>
201-
<SheetSection>
202-
{selectedLint && projectRef && (
203-
<LintDetail
204-
lint={selectedLint}
205-
projectRef={projectRef!}
206-
onAskAssistant={() => setSelectedLint(null)}
207-
/>
208-
)}
209-
</SheetSection>
210-
</>
211-
)}
212-
</SheetContent>
213-
</Sheet>
214172
</>
215173
) : (
216174
<EmptyState />

apps/studio/components/interfaces/Linter/Linter.utils.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ export const LintCTA = ({
312312

313313
return (
314314
<Button asChild type="default">
315-
<Link href={link} target="_blank" rel="noreferrer" className="no-underline">
315+
<Link href={link} rel="noreferrer" className="no-underline">
316316
{linkText}
317317
</Link>
318318
</Button>

apps/studio/components/layouts/AdvisorsLayout/AdvisorsLayout.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,23 @@
11
import { useRouter } from 'next/router'
22
import { PropsWithChildren } from 'react'
33

4-
import { useIsAdvisorRulesEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
5-
import { ProductMenu } from 'components/ui/ProductMenu'
6-
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
74
import { withAuth } from 'hooks/misc/withAuth'
85
import { ProjectLayout } from '../ProjectLayout/ProjectLayout'
9-
import { generateAdvisorsMenu } from './AdvisorsMenu.utils'
6+
import { AdvisorsSidebarMenu } from './AdvisorsSidebarMenu'
107

118
export interface AdvisorsLayoutProps {
129
title?: string
1310
}
1411

1512
const AdvisorsLayout = ({ children }: PropsWithChildren<AdvisorsLayoutProps>) => {
16-
const { data: project } = useSelectedProjectQuery()
17-
const advisorRules = useIsAdvisorRulesEnabled()
18-
1913
const router = useRouter()
2014
const page = router.pathname.split('/')[4]
2115

2216
return (
2317
<ProjectLayout
2418
isLoading={false}
2519
product="Advisors"
26-
productMenu={
27-
<ProductMenu page={page} menu={generateAdvisorsMenu(project, { advisorRules })} />
28-
}
20+
productMenu={<AdvisorsSidebarMenu page={page} />}
2921
>
3022
{children}
3123
</ProjectLayout>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ProductMenu } from 'components/ui/ProductMenu'
2+
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
3+
import { Badge, Button } from 'ui'
4+
import { FeaturePreviewSidebarPanel } from '../../ui/FeaturePreviewSidebarPanel'
5+
import { generateAdvisorsMenu } from './AdvisorsMenu.utils'
6+
import { useIsAdvisorRulesEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
7+
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
8+
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
9+
10+
interface AdvisorsSidebarMenuProps {
11+
page?: string
12+
}
13+
14+
export function AdvisorsSidebarMenu({ page }: AdvisorsSidebarMenuProps) {
15+
const { data: project } = useSelectedProjectQuery()
16+
const advisorRules = useIsAdvisorRulesEnabled()
17+
const { toggleSidebar } = useSidebarManagerSnapshot()
18+
19+
const handleOpenAdvisor = () => {
20+
toggleSidebar(SIDEBAR_KEYS.ADVISOR_PANEL)
21+
}
22+
23+
return (
24+
<div className="pb-12 relative">
25+
<FeaturePreviewSidebarPanel
26+
className="mx-4 mt-4"
27+
title="Moving to the toolbar"
28+
description="Advisors are now available in the top toolbar for quicker access across the dashboard"
29+
illustration={<Badge variant="brand">New Location</Badge>}
30+
actions={
31+
<Button size="tiny" type="default" onClick={handleOpenAdvisor}>
32+
Try it now
33+
</Button>
34+
}
35+
/>
36+
37+
<ProductMenu page={page} menu={generateAdvisorsMenu(project, { advisorRules })} />
38+
</div>
39+
)
40+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Lightbulb } from 'lucide-react'
2+
3+
import { useParams } from 'common'
4+
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
5+
import { useProjectLintsQuery } from 'data/lint/lint-query'
6+
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
7+
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
8+
import { cn } from 'ui'
9+
10+
export const AdvisorButton = () => {
11+
const { ref: projectRef } = useParams()
12+
const { toggleSidebar, activeSidebar } = useSidebarManagerSnapshot()
13+
const { data: lints } = useProjectLintsQuery({ projectRef })
14+
15+
const hasCriticalIssues = Array.isArray(lints) && lints.some((lint) => lint.level === 'ERROR')
16+
17+
const isOpen = activeSidebar?.id === SIDEBAR_KEYS.ADVISOR_PANEL
18+
19+
const handleClick = () => {
20+
toggleSidebar(SIDEBAR_KEYS.ADVISOR_PANEL)
21+
}
22+
23+
return (
24+
<div className="relative">
25+
<ButtonTooltip
26+
type="outline"
27+
size="tiny"
28+
id="advisor-center-trigger"
29+
className={cn(
30+
'rounded-full w-[32px] h-[32px] flex items-center justify-center p-0 group',
31+
isOpen && 'bg-foreground text-background'
32+
)}
33+
onClick={handleClick}
34+
tooltip={{
35+
content: {
36+
text: 'Advisor Center',
37+
},
38+
}}
39+
>
40+
<Lightbulb
41+
size={16}
42+
strokeWidth={1.5}
43+
className={cn(
44+
'text-foreground-light group-hover:text-foreground',
45+
isOpen && 'text-background group-hover:text-background'
46+
)}
47+
/>
48+
</ButtonTooltip>
49+
{hasCriticalIssues && (
50+
<span className="absolute top-1.5 right-1.5 w-1.5 h-1.5 rounded-full bg-destructive" />
51+
)}
52+
</div>
53+
)
54+
}

apps/studio/components/layouts/AppLayout/AssistantButton.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,28 @@ import { LOCAL_STORAGE_KEYS } from 'common'
22
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
33
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
44
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
5-
import { AiIconAnimation, KeyboardShortcut } from 'ui'
6-
import { SIDEBAR_KEYS } from '../ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
75
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
6+
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
7+
import { AiIconAnimation, cn, KeyboardShortcut } from 'ui'
88

99
export const AssistantButton = () => {
10-
const { toggleSidebar } = useSidebarManagerSnapshot()
10+
const { activeSidebar, toggleSidebar } = useSidebarManagerSnapshot()
1111
const [isAIAssistantHotkeyEnabled] = useLocalStorageQuery<boolean>(
1212
LOCAL_STORAGE_KEYS.HOTKEY_SIDEBAR(SIDEBAR_KEYS.AI_ASSISTANT),
1313
true
1414
)
1515

16+
const isOpen = activeSidebar?.id === SIDEBAR_KEYS.AI_ASSISTANT
17+
1618
return (
1719
<ButtonTooltip
18-
type="text"
20+
type="outline"
1921
size="tiny"
2022
id="assistant-trigger"
21-
className="rounded-none w-[32px] h-[30px] flex items-center justify-center p-0 hover:bg-brand-400"
23+
className={cn(
24+
'rounded-full w-[32px] h-[32px] flex items-center justify-center p-0',
25+
isOpen && 'bg-foreground text-background'
26+
)}
2227
onClick={() => {
2328
toggleSidebar(SIDEBAR_KEYS.AI_ASSISTANT)
2429
}}
@@ -33,7 +38,11 @@ export const AssistantButton = () => {
3338
},
3439
}}
3540
>
36-
<AiIconAnimation allowHoverEffect={false} size={16} />
41+
<AiIconAnimation
42+
allowHoverEffect={false}
43+
size={16}
44+
className={cn(isOpen && 'text-background')}
45+
/>
3746
</ButtonTooltip>
3847
)
3948
}

0 commit comments

Comments
 (0)