Merge branch 'develop' into fix/issue_21600

This commit is contained in:
SamPhillemon 2024-10-07 19:38:42 +05:30 committed by GitHub
commit c7c382f99a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 169 additions and 37 deletions

View File

@ -0,0 +1,5 @@
import { Command } from 'commander';
export function defineCloudNamespace(command: Command): Command {
return command.command('cloud').description('Manage Strapi Cloud projects');
}

View File

@ -0,0 +1,7 @@
import { Command } from 'commander';
import { defineCloudNamespace } from '../cloud/command';
export function createEnvironmentCommand(command: Command): Command {
const cloud = defineCloudNamespace(command);
return cloud.command('environment').description('Manage environments for a Strapi Cloud project');
}

View File

@ -0,0 +1,66 @@
import chalk from 'chalk';
import type { CLIContext } from '../../types';
import { cloudApiFactory, local, tokenServiceFactory } from '../../services';
import { promptLogin } from '../../login/action';
import { trackEvent } from '../../utils/analytics';
async function getProject(ctx: CLIContext) {
const { project } = await local.retrieve();
if (!project) {
ctx.logger.warn(
`\nWe couldn't find a valid local project config.\nPlease link your local project to an existing Strapi Cloud project using the ${chalk.cyan(
'link'
)} command`
);
process.exit(1);
}
return project;
}
export default async (ctx: CLIContext) => {
const { getValidToken } = await tokenServiceFactory(ctx);
const token = await getValidToken(ctx, promptLogin);
const { logger } = ctx;
if (!token) {
return;
}
const project = await getProject(ctx);
if (!project) {
ctx.logger.debug(`No valid local project configuration was found.`);
return;
}
const cloudApiService = await cloudApiFactory(ctx, token);
const spinner = logger.spinner('Fetching environments...').start();
await trackEvent(ctx, cloudApiService, 'willListEnvironment', {
projectInternalName: project.name,
});
try {
const {
data: { data: environmentsList },
} = await cloudApiService.listEnvironments({ name: project.name });
spinner.succeed();
logger.log(environmentsList);
await trackEvent(ctx, cloudApiService, 'didListEnvironment', {
projectInternalName: project.name,
});
} catch (e: any) {
if (e.response && e.response.status === 404) {
spinner.succeed();
logger.warn(
`\nThe project associated with this folder does not exist in Strapi Cloud. \nPlease link your local project to an existing Strapi Cloud project using the ${chalk.cyan(
'link'
)} command`
);
} else {
spinner.fail('An error occurred while fetching environments data from Strapi Cloud.');
logger.debug('Failed to list environments', e);
}
await trackEvent(ctx, cloudApiService, 'didNotListEnvironment', {
projectInternalName: project.name,
});
}
};

View File

@ -0,0 +1,25 @@
import { type StrapiCloudCommand } from '../../types';
import { runAction } from '../../utils/helpers';
import action from './action';
import { defineCloudNamespace } from '../../cloud/command';
const command: StrapiCloudCommand = ({ command, ctx }) => {
const cloud = defineCloudNamespace(command);
cloud
.command('environments')
.description('Alias for cloud environment list')
.action(() => runAction('list', action)(ctx));
const environment = cloud
.command('environment')
.description('Manage environments for a Strapi Cloud project');
environment
.command('list')
.description('List Strapi Cloud project environments')
.option('-d, --debug', 'Enable debugging mode with verbose logs')
.option('-s, --silent', "Don't log anything")
.action(() => runAction('list', action)(ctx));
};
export default command;

View File

@ -0,0 +1,12 @@
import action from './action';
import command from './command';
import type { StrapiCloudCommandInfo } from '../../types';
export { action, command };
export default {
name: 'list-environments',
description: 'List Strapi Cloud environments',
action,
command,
} as StrapiCloudCommandInfo;

View File

@ -6,6 +6,7 @@ import login from './login';
import logout from './logout'; import logout from './logout';
import createProject from './create-project'; import createProject from './create-project';
import listProjects from './list-projects'; import listProjects from './list-projects';
import listEnvironments from './environment/list';
import { CLIContext } from './types'; import { CLIContext } from './types';
import { getLocalConfig, saveLocalConfig } from './config/local'; import { getLocalConfig, saveLocalConfig } from './config/local';
@ -16,9 +17,10 @@ export const cli = {
logout, logout,
createProject, createProject,
listProjects, listProjects,
listEnvironments,
}; };
const cloudCommands = [deployProject, link, login, logout, listProjects]; const cloudCommands = [deployProject, link, login, logout, listProjects, listEnvironments];
async function initCloudCLIConfig() { async function initCloudCLIConfig() {
const localConfig = await getLocalConfig(); const localConfig = await getLocalConfig();

View File

@ -19,6 +19,8 @@ export type ProjectInfos = {
url?: string; url?: string;
}; };
export type EnvironmentInfo = Record<string, unknown>;
export type ProjectInput = Omit<ProjectInfos, 'id'>; export type ProjectInput = Omit<ProjectInfos, 'id'>;
export type DeployResponse = { export type DeployResponse = {
@ -32,6 +34,12 @@ export type ListProjectsResponse = {
}; };
}; };
export type ListEnvironmentsResponse = {
data: {
data: EnvironmentInfo[] | Record<string, never>;
};
};
export type ListLinkProjectsResponse = { export type ListLinkProjectsResponse = {
data: { data: {
data: ProjectInfos[] | Record<string, never>; data: ProjectInfos[] | Record<string, never>;
@ -78,6 +86,8 @@ export interface CloudApiService {
listLinkProjects(): Promise<AxiosResponse<ListLinkProjectsResponse>>; listLinkProjects(): Promise<AxiosResponse<ListLinkProjectsResponse>>;
listEnvironments(project: { name: string }): Promise<AxiosResponse<ListEnvironmentsResponse>>;
getProject(project: { name: string }): Promise<AxiosResponse<GetProjectResponse>>; getProject(project: { name: string }): Promise<AxiosResponse<GetProjectResponse>>;
track(event: string, payload?: TrackPayload): Promise<AxiosResponse<void>>; track(event: string, payload?: TrackPayload): Promise<AxiosResponse<void>>;
@ -197,6 +207,23 @@ export async function cloudApiFactory(
} }
}, },
async listEnvironments({ name }): Promise<AxiosResponse<ListEnvironmentsResponse>> {
try {
const response = await axiosCloudAPI.get(`/projects/${name}/environments`);
if (response.status !== 200) {
throw new Error('Error fetching cloud environments from the server.');
}
return response;
} catch (error) {
logger.debug(
"🥲 Oops! Couldn't retrieve your project's environments from the server. Please try again."
);
throw error;
}
},
async getProject({ name }): Promise<AxiosResponse<GetProjectResponse>> { async getProject({ name }): Promise<AxiosResponse<GetProjectResponse>> {
try { try {
const response = await axiosCloudAPI.get(`/projects/${name}`); const response = await axiosCloudAPI.get(`/projects/${name}`);

View File

@ -39,6 +39,10 @@ export type StrapiCloudCommand = (params: {
ctx: CLIContext; ctx: CLIContext;
}) => void | Command | Promise<Command | void>; }) => void | Command | Promise<Command | void>;
export type StrapiCloudNamespaceCommand = (params: {
command: Command;
}) => void | Command | Promise<Command | void>;
export type StrapiCloudCommandInfo = { export type StrapiCloudCommandInfo = {
name: string; name: string;
description: string; description: string;

View File

@ -66,7 +66,10 @@ const NpmPackageCard = ({
}`; }`;
const versionRange = semver.validRange(attributes.strapiVersion); const versionRange = semver.validRange(attributes.strapiVersion);
const isCompatible = semver.satisfies(strapiAppVersion ?? '', versionRange ?? '');
const isCompatible = versionRange
? semver.satisfies(strapiAppVersion ?? '', versionRange)
: false;
return ( return (
<Flex <Flex
@ -272,28 +275,17 @@ const CardButton = ({
return ( return (
<Tooltip <Tooltip
data-testid={`tooltip-${pluginName}`} data-testid={`tooltip-${pluginName}`}
label={ label={formatMessage(
!versionRange {
? formatMessage( id: 'admin.pages.MarketPlacePage.plugin.version',
{ defaultMessage:
id: 'admin.pages.MarketPlacePage.plugin.version.null', 'Update your Strapi version: "{strapiAppVersion}" to: "{versionRange}"',
defaultMessage: },
'Unable to verify compatibility with your Strapi version: "{strapiAppVersion}"', {
}, strapiAppVersion,
{ strapiAppVersion } versionRange,
) }
: formatMessage( )}
{
id: 'admin.pages.MarketPlacePage.plugin.version',
defaultMessage:
'Update your Strapi version: "{strapiAppVersion}" to: "{versionRange}"',
},
{
strapiAppVersion,
versionRange,
}
)
}
> >
<span> <span>
<Button <Button

View File

@ -62,25 +62,17 @@ describe('Marketplace page - layout', () => {
expect(button).not.toBeInTheDocument(); expect(button).not.toBeInTheDocument();
}); });
it('shows compatibility tooltip message when no version provided', async () => { it('Does not show (copy install command) button', async () => {
const { findByTestId, findAllByTestId, user } = render(); const { findAllByTestId } = render();
const alreadyInstalledCard = (await findAllByTestId('npm-package-card')).find((div) => const alreadyInstalledCard = (await findAllByTestId('npm-package-card')).find((div) =>
div.innerHTML.includes('Config Sync') div.innerHTML.includes('Config Sync')
)!; )!;
const button = within(alreadyInstalledCard) const button = within(alreadyInstalledCard).queryByText(/copy install command/i);
.getByText(/copy install command/i)
.closest('button')!;
await user.hover(button); // Assert that the button does not show
const tooltip = await findByTestId(`tooltip-Config Sync`); expect(button).not.toBeInTheDocument();
expect(button).toBeEnabled();
expect(tooltip).toBeInTheDocument();
expect(tooltip).toHaveTextContent(
'Unable to verify compatibility with your Strapi version: "4.1.0"'
);
}); });
it('handles production environment', async () => { it('handles production environment', async () => {