diff --git a/CHANGELOG.md b/CHANGELOG.md index fe4a707301..9264e1d21c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ This is the log of notable changes to EAS CLI and related packages. ### 🎉 New features - [eas-cli] Add `eas integrations:supabase:connect` command. +- [eas-cli] Add `eas integrations:supabase:dashboard` command. - [build-tools] Add `ref` input to the `eas/checkout` step to check out a different git ref (branch, tag, or commit SHA) than the one that triggered the job. ([#4035](https://github.com/expo/eas-cli/pull/4035) by [@sswrk](https://github.com/sswrk)) diff --git a/packages/eas-cli/src/commands/integrations/supabase/__tests__/dashboard.test.ts b/packages/eas-cli/src/commands/integrations/supabase/__tests__/dashboard.test.ts new file mode 100644 index 0000000000..4f9c04e538 --- /dev/null +++ b/packages/eas-cli/src/commands/integrations/supabase/__tests__/dashboard.test.ts @@ -0,0 +1,83 @@ +import openBrowserAsync from 'better-opn'; + +import { getMockOclifConfig } from '../../../../__tests__/commands/utils'; +import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient'; +import { testProjectId } from '../../../../credentials/__tests__/fixtures-constants'; +import { SupabaseQuery } from '../../../../graphql/queries/SupabaseQuery'; +import { SupabaseProjectData } from '../../../../graphql/types/SupabaseConnection'; +import Log from '../../../../log'; +import { printJsonOnlyOutput } from '../../../../utils/json'; +import IntegrationsSupabaseDashboard from '../dashboard'; + +jest.mock('better-opn'); +jest.mock('../../../../graphql/queries/SupabaseQuery'); +jest.mock('../../../../log'); +jest.mock('../../../../utils/json'); +jest.mock('../../../../ora', () => ({ + ora: jest.fn(() => ({ + start: jest.fn().mockReturnThis(), + succeed: jest.fn().mockReturnThis(), + fail: jest.fn().mockReturnThis(), + })), +})); + +describe(IntegrationsSupabaseDashboard, () => { + const graphqlClient = {} as ExpoGraphqlClient; + const mockConfig = getMockOclifConfig(); + + const mockProject: SupabaseProjectData = { + id: 'project-1', + supabaseProjectRef: 'abcdefghijklmnop', + supabaseProjectName: 'Test App', + supabaseProjectUrl: 'https://abcdefghijklmnop.supabase.co', + supabaseRegion: 'us-east-1', + createdAt: '2024-01-01T00:00:00.000Z', + updatedAt: '2024-01-01T00:00:00.000Z', + }; + + function createCommand(argv: string[]): IntegrationsSupabaseDashboard { + const command = new IntegrationsSupabaseDashboard(argv, mockConfig); + jest.spyOn(command as any, 'getContextAsync').mockReturnValue({ + privateProjectConfig: { + projectId: testProjectId, + exp: { slug: 'testapp' }, + }, + loggedIn: { graphqlClient }, + } as never); + return command; + } + + beforeEach(() => { + jest.clearAllMocks(); + jest.spyOn(Log, 'log').mockImplementation(() => {}); + jest.spyOn(Log, 'warn').mockImplementation(() => {}); + jest.mocked(SupabaseQuery.getSupabaseProjectByAppIdAsync).mockResolvedValue(mockProject); + jest.mocked(openBrowserAsync).mockResolvedValue(true as never); + }); + + it('opens the Supabase project dashboard', async () => { + await createCommand([]).runAsync(); + + expect(openBrowserAsync).toHaveBeenCalledWith( + 'https://supabase.com/dashboard/project/abcdefghijklmnop' + ); + }); + + it('prints the dashboard URL as JSON', async () => { + await createCommand(['--json']).runAsync(); + + expect(printJsonOnlyOutput).toHaveBeenCalledWith({ + dashboardUrl: 'https://supabase.com/dashboard/project/abcdefghijklmnop', + }); + expect(openBrowserAsync).not.toHaveBeenCalled(); + }); + + it('warns when no project is linked', async () => { + jest.mocked(SupabaseQuery.getSupabaseProjectByAppIdAsync).mockResolvedValue(null); + + await createCommand([]).runAsync(); + + expect(Log.warn).toHaveBeenCalledWith(expect.stringContaining('No Supabase project')); + expect(openBrowserAsync).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/eas-cli/src/commands/integrations/supabase/dashboard.ts b/packages/eas-cli/src/commands/integrations/supabase/dashboard.ts new file mode 100644 index 0000000000..e86cfdc6b5 --- /dev/null +++ b/packages/eas-cli/src/commands/integrations/supabase/dashboard.ts @@ -0,0 +1,82 @@ +import openBrowserAsync from 'better-opn'; + +import EasCommand from '../../../commandUtils/EasCommand'; +import { + EasNonInteractiveAndJsonFlags, + resolveNonInteractiveAndJsonFlags, +} from '../../../commandUtils/flags'; +import { + getSupabaseProjectDashboardUrl, + logNoSupabaseProject, +} from '../../../commandUtils/supabase'; +import { SupabaseQuery } from '../../../graphql/queries/SupabaseQuery'; +import Log from '../../../log'; +import { ora } from '../../../ora'; +import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json'; + +export default class IntegrationsSupabaseDashboard extends EasCommand { + static override description = + "Open this app's primary Supabase project in the dashboard (or print the URL with --non-interactive)."; + + static override examples = [ + '<%= config.bin %> <%= command.id %>', + '<%= config.bin %> <%= command.id %> --non-interactive', + ]; + + static override flags = { + ...EasNonInteractiveAndJsonFlags, + }; + + static override contextDefinition = { + ...this.ContextOptions.ProjectConfig, + }; + + async runAsync(): Promise { + const { flags } = await this.parse(IntegrationsSupabaseDashboard); + const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags); + if (jsonFlag) { + enableJsonOutput(); + } + + const { + privateProjectConfig: { projectId, exp }, + loggedIn: { graphqlClient }, + } = await this.getContextAsync(IntegrationsSupabaseDashboard, { + nonInteractive, + withServerSideEnvironment: null, + }); + + const project = await SupabaseQuery.getSupabaseProjectByAppIdAsync(graphqlClient, projectId); + if (!project) { + if (jsonFlag) { + printJsonOnlyOutput({ dashboardUrl: null }); + } else { + logNoSupabaseProject(exp.slug); + } + return; + } + + const dashboardUrl = getSupabaseProjectDashboardUrl(project); + if (jsonFlag) { + printJsonOnlyOutput({ dashboardUrl }); + return; + } + if (nonInteractive) { + Log.log(dashboardUrl); + return; + } + + const spinner = ora('Opening your Supabase dashboard').start(); + try { + const opened = await openBrowserAsync(dashboardUrl); + if (opened) { + spinner.succeed('Opened your Supabase dashboard'); + } else { + spinner.fail(`Unable to open a web browser. Supabase dashboard: ${dashboardUrl}`); + } + } catch (error) { + spinner.fail(`Unable to open a web browser. Supabase dashboard: ${dashboardUrl}`); + throw error; + } + } +}