mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-05 23:26:38 +08:00
[优化] 上传部署包功能及相关界面优化
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
interface EmptyStateProps {
|
interface EmptyStateProps {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmptyState({ title, description }: EmptyStateProps) {
|
export function EmptyState({ title, description, children }: EmptyStateProps) {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl border border-dashed border-[var(--border-default)] bg-[var(--surface-muted)] px-5 py-6 text-sm">
|
<div className="rounded-2xl border border-dashed border-[var(--border-default)] bg-[var(--surface-muted)] px-5 py-6 text-sm">
|
||||||
<p className="text-base font-semibold text-[var(--foreground-primary)]">
|
<p className="text-base font-semibold text-[var(--foreground-primary)]">
|
||||||
@@ -14,6 +17,8 @@ export function EmptyState({ title, description }: EmptyStateProps) {
|
|||||||
{description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
{children ? <div className="mt-4">{children}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,10 +134,8 @@ export function PagesPage() {
|
|||||||
export function PagesProjectDetailPage({ projectId }: { projectId: string }) {
|
export function PagesProjectDetailPage({ projectId }: { projectId: string }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [file, setFile] = useState<File | null>(null);
|
|
||||||
const [entryFile, setEntryFile] = useState('index.html');
|
|
||||||
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
|
|
||||||
const [isEditModalOpen, setEditModalOpen] = useState(false);
|
const [isEditModalOpen, setEditModalOpen] = useState(false);
|
||||||
|
const [isUploadModalOpen, setUploadModalOpen] = useState(false);
|
||||||
|
|
||||||
const parsedProjectId = Number(projectId);
|
const parsedProjectId = Number(projectId);
|
||||||
const projectQuery = useQuery({
|
const projectQuery = useQuery({
|
||||||
@@ -151,29 +149,6 @@ export function PagesProjectDetailPage({ projectId }: { projectId: string }) {
|
|||||||
enabled: projectId !== '' && Number.isFinite(parsedProjectId),
|
enabled: projectId !== '' && Number.isFinite(parsedProjectId),
|
||||||
});
|
});
|
||||||
|
|
||||||
const uploadMutation = useMutation({
|
|
||||||
mutationFn: () => {
|
|
||||||
if (!file) {
|
|
||||||
throw new Error('请选择 zip 文件');
|
|
||||||
}
|
|
||||||
setUploadProgress(0);
|
|
||||||
return uploadPagesDeployment(parsedProjectId, file, entryFile, (percent) => {
|
|
||||||
setUploadProgress(percent);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
setFile(null);
|
|
||||||
setUploadProgress(null);
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: deploymentsQueryKey(parsedProjectId),
|
|
||||||
});
|
|
||||||
queryClient.invalidateQueries({ queryKey: projectQueryKey(projectId) });
|
|
||||||
queryClient.invalidateQueries({ queryKey: projectsQueryKey });
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
setUploadProgress(null);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const activateMutation = useMutation({
|
const activateMutation = useMutation({
|
||||||
mutationFn: (deploymentId: number) =>
|
mutationFn: (deploymentId: number) =>
|
||||||
activatePagesDeployment(parsedProjectId, deploymentId),
|
activatePagesDeployment(parsedProjectId, deploymentId),
|
||||||
@@ -262,6 +237,12 @@ export function PagesProjectDetailPage({ projectId }: { projectId: string }) {
|
|||||||
>
|
>
|
||||||
编辑项目
|
编辑项目
|
||||||
</SecondaryButton>
|
</SecondaryButton>
|
||||||
|
<PrimaryButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => setUploadModalOpen(true)}
|
||||||
|
>
|
||||||
|
上传部署包
|
||||||
|
</PrimaryButton>
|
||||||
<DangerButton
|
<DangerButton
|
||||||
type="button"
|
type="button"
|
||||||
disabled={deleteProjectMutation.isPending}
|
disabled={deleteProjectMutation.isPending}
|
||||||
@@ -273,149 +254,271 @@ export function PagesProjectDetailPage({ projectId }: { projectId: string }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AppCard
|
||||||
<div className="grid gap-6 lg:grid-cols-[minmax(0,0.85fr)_minmax(0,1.15fr)]">
|
title="部署历史"
|
||||||
<AppCard
|
description="部署不可变;激活后发布配置,Agent 才会拉取并切换静态资源。"
|
||||||
title="上传部署包"
|
action={
|
||||||
description="上传已构建的 zip 静态资源包,默认入口 index.html。"
|
(deploymentsQuery.data ?? []).length > 0 ? (
|
||||||
>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<ResourceField
|
|
||||||
label="部署包"
|
|
||||||
hint="仅支持 zip,Server 会校验文件数量、体积、路径逃逸和入口文件。"
|
|
||||||
>
|
|
||||||
<ResourceInput
|
|
||||||
type="file"
|
|
||||||
accept=".zip,application/zip"
|
|
||||||
onChange={(event) => setFile(event.target.files?.[0] ?? null)}
|
|
||||||
/>
|
|
||||||
</ResourceField>
|
|
||||||
<ResourceField label="入口文件">
|
|
||||||
<ResourceInput
|
|
||||||
value={entryFile}
|
|
||||||
onChange={(event) => setEntryFile(event.target.value)}
|
|
||||||
/>
|
|
||||||
</ResourceField>
|
|
||||||
{uploadProgress !== null && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center justify-between text-xs font-medium text-[var(--foreground-secondary)]">
|
|
||||||
<span>上传进度</span>
|
|
||||||
<span>{uploadProgress}%</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-2 w-full overflow-hidden rounded-full bg-[var(--surface-muted)]">
|
|
||||||
<div
|
|
||||||
className="h-full rounded-full bg-[var(--brand-primary)] transition-all duration-300 ease-out"
|
|
||||||
style={{ width: `${uploadProgress}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!file || uploadMutation.isPending}
|
onClick={() => setUploadModalOpen(true)}
|
||||||
onClick={() => uploadMutation.mutate()}
|
|
||||||
>
|
>
|
||||||
{uploadMutation.isPending
|
上传部署包
|
||||||
? `上传中 (${uploadProgress ?? 0}%)...`
|
|
||||||
: '上传部署'}
|
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
{uploadMutation.error ? (
|
) : null
|
||||||
<p className="text-sm text-[var(--status-danger-foreground)]">
|
}
|
||||||
{uploadMutation.error.message}
|
>
|
||||||
</p>
|
{deploymentsQuery.isLoading ? (
|
||||||
) : null}
|
<p className="text-sm text-[var(--foreground-secondary)]">
|
||||||
</div>
|
加载中...
|
||||||
</AppCard>
|
</p>
|
||||||
|
) : deploymentsQuery.isError ? (
|
||||||
<AppCard
|
<p className="text-sm text-[var(--status-danger-foreground)]">
|
||||||
title="部署历史"
|
{deploymentsQuery.error.message}
|
||||||
description="部署不可变;激活后发布配置,Agent 才会拉取并切换静态资源。"
|
</p>
|
||||||
>
|
) : (deploymentsQuery.data ?? []).length === 0 ? (
|
||||||
{deploymentsQuery.isLoading ? (
|
<EmptyState
|
||||||
<p className="text-sm text-[var(--foreground-secondary)]">
|
title="暂无部署"
|
||||||
加载中...
|
description="上传 zip 部署包后,可以在这里激活某个部署版本。"
|
||||||
</p>
|
>
|
||||||
) : deploymentsQuery.isError ? (
|
<PrimaryButton
|
||||||
<p className="text-sm text-[var(--status-danger-foreground)]">
|
type="button"
|
||||||
{deploymentsQuery.error.message}
|
onClick={() => setUploadModalOpen(true)}
|
||||||
</p>
|
>
|
||||||
) : (deploymentsQuery.data ?? []).length === 0 ? (
|
上传部署包
|
||||||
<EmptyState
|
</PrimaryButton>
|
||||||
title="暂无部署"
|
</EmptyState>
|
||||||
description="上传 zip 部署包后,可以在这里激活某个部署版本。"
|
) : (
|
||||||
/>
|
<div className="overflow-hidden rounded-2xl border border-[var(--border-default)]">
|
||||||
) : (
|
{(deploymentsQuery.data ?? []).map((deployment) => (
|
||||||
<div className="overflow-hidden rounded-2xl border border-[var(--border-default)]">
|
<div
|
||||||
{(deploymentsQuery.data ?? []).map((deployment) => (
|
key={deployment.id}
|
||||||
<div
|
className="flex flex-col gap-3 border-b border-[var(--border-default)] p-4 last:border-b-0 md:flex-row md:items-center md:justify-between"
|
||||||
key={deployment.id}
|
>
|
||||||
className="flex flex-col gap-3 border-b border-[var(--border-default)] p-4 last:border-b-0 md:flex-row md:items-center md:justify-between"
|
<div>
|
||||||
>
|
<p className="text-sm font-medium text-[var(--foreground-primary)]">
|
||||||
<div>
|
#{deployment.deployment_number}{' '}
|
||||||
<p className="text-sm font-medium text-[var(--foreground-primary)]">
|
{deployment.status === 'active' ? '· 已激活' : ''}
|
||||||
#{deployment.deployment_number}{' '}
|
</p>
|
||||||
{deployment.status === 'active' ? '· 已激活' : ''}
|
<p className="mt-1 text-xs text-[var(--foreground-secondary)]">
|
||||||
</p>
|
{deployment.checksum.slice(0, 16)} ·{' '}
|
||||||
<p className="mt-1 text-xs text-[var(--foreground-secondary)]">
|
{deployment.file_count} files ·{' '}
|
||||||
{deployment.checksum.slice(0, 16)} ·{' '}
|
{formatBytes(deployment.total_size)}
|
||||||
{deployment.file_count} files ·{' '}
|
</p>
|
||||||
{formatBytes(deployment.total_size)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<SecondaryButton
|
|
||||||
type="button"
|
|
||||||
disabled={
|
|
||||||
deployment.status === 'active' ||
|
|
||||||
activateMutation.isPending
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
window.confirm(
|
|
||||||
`确认激活部署 #${deployment.deployment_number} 吗?`,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
activateMutation.mutate(deployment.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
激活
|
|
||||||
</SecondaryButton>
|
|
||||||
<DangerButton
|
|
||||||
type="button"
|
|
||||||
disabled={
|
|
||||||
deployment.status === 'active' ||
|
|
||||||
deleteDeploymentMutation.isPending
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
window.confirm(
|
|
||||||
`确认删除部署 #${deployment.deployment_number} 吗?`,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
deleteDeploymentMutation.mutate(deployment.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</DangerButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<div className="flex gap-2">
|
||||||
</div>
|
<SecondaryButton
|
||||||
)}
|
type="button"
|
||||||
</AppCard>
|
disabled={
|
||||||
</div>
|
deployment.status === 'active' ||
|
||||||
|
activateMutation.isPending
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
window.confirm(
|
||||||
|
`确认激活部署 #${deployment.deployment_number} 吗?`,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
activateMutation.mutate(deployment.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
激活
|
||||||
|
</SecondaryButton>
|
||||||
|
<DangerButton
|
||||||
|
type="button"
|
||||||
|
disabled={
|
||||||
|
deployment.status === 'active' ||
|
||||||
|
deleteDeploymentMutation.isPending
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
window.confirm(
|
||||||
|
`确认删除部署 #${deployment.deployment_number} 吗?`,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
deleteDeploymentMutation.mutate(deployment.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</DangerButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AppCard>
|
||||||
|
|
||||||
<PagesProjectEditModal
|
<PagesProjectEditModal
|
||||||
isOpen={isEditModalOpen}
|
isOpen={isEditModalOpen}
|
||||||
onClose={() => setEditModalOpen(false)}
|
onClose={() => setEditModalOpen(false)}
|
||||||
project={project}
|
project={project}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PagesDeploymentUploadModal
|
||||||
|
isOpen={isUploadModalOpen}
|
||||||
|
onClose={() => setUploadModalOpen(false)}
|
||||||
|
projectId={parsedProjectId}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function PagesDeploymentUploadModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
projectId,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
projectId: number;
|
||||||
|
}) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [entryFile, setEntryFile] = useState('index.html');
|
||||||
|
const [uploadProgress, setUploadProgress] = useState<number | null>(null);
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
setFile(null);
|
||||||
|
setEntryFile('index.html');
|
||||||
|
setUploadProgress(null);
|
||||||
|
setErrorMessage(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
resetForm();
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const uploadMutation = useMutation({
|
||||||
|
mutationFn: async ({ shouldActivate }: { shouldActivate: boolean }) => {
|
||||||
|
if (!file) {
|
||||||
|
throw new Error('请选择 zip 文件');
|
||||||
|
}
|
||||||
|
setUploadProgress(0);
|
||||||
|
setErrorMessage(null);
|
||||||
|
|
||||||
|
const deployment = await uploadPagesDeployment(
|
||||||
|
projectId,
|
||||||
|
file,
|
||||||
|
entryFile,
|
||||||
|
(percent) => {
|
||||||
|
setUploadProgress(percent);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldActivate) {
|
||||||
|
await activatePagesDeployment(projectId, deployment.id);
|
||||||
|
}
|
||||||
|
return deployment;
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
resetForm();
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: deploymentsQueryKey(projectId),
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: projectQueryKey(projectId),
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: projectsQueryKey,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
setUploadProgress(null);
|
||||||
|
setErrorMessage(error instanceof Error ? error.message : '上传失败');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleUploadOnly = () => {
|
||||||
|
uploadMutation.mutate({ shouldActivate: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUploadAndDeploy = () => {
|
||||||
|
uploadMutation.mutate({ shouldActivate: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
title="上传部署包"
|
||||||
|
description="上传已构建的 zip 静态资源包,默认入口为 index.html。"
|
||||||
|
footer={
|
||||||
|
<div className="flex flex-wrap justify-end gap-3">
|
||||||
|
<SecondaryButton
|
||||||
|
type="button"
|
||||||
|
onClick={handleClose}
|
||||||
|
disabled={uploadMutation.isPending}
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</SecondaryButton>
|
||||||
|
<SecondaryButton
|
||||||
|
type="button"
|
||||||
|
disabled={!file || uploadMutation.isPending}
|
||||||
|
onClick={handleUploadOnly}
|
||||||
|
>
|
||||||
|
{uploadMutation.isPending && !uploadMutation.variables?.shouldActivate
|
||||||
|
? `上传中 (${uploadProgress ?? 0}%)...`
|
||||||
|
: '上传'}
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton
|
||||||
|
type="button"
|
||||||
|
disabled={!file || uploadMutation.isPending}
|
||||||
|
onClick={handleUploadAndDeploy}
|
||||||
|
>
|
||||||
|
{uploadMutation.isPending && uploadMutation.variables?.shouldActivate
|
||||||
|
? `上传并部署中 (${uploadProgress ?? 0}%)...`
|
||||||
|
: '上传并部署'}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<ResourceField
|
||||||
|
label="部署包"
|
||||||
|
hint="仅支持 zip,Server 会校验文件数量、体积、路径逃逸和入口文件。"
|
||||||
|
>
|
||||||
|
<ResourceInput
|
||||||
|
type="file"
|
||||||
|
accept=".zip,application/zip"
|
||||||
|
onChange={(event) => setFile(event.target.files?.[0] ?? null)}
|
||||||
|
/>
|
||||||
|
</ResourceField>
|
||||||
|
<ResourceField label="入口文件">
|
||||||
|
<ResourceInput
|
||||||
|
value={entryFile}
|
||||||
|
onChange={(event) => setEntryFile(event.target.value)}
|
||||||
|
/>
|
||||||
|
</ResourceField>
|
||||||
|
{uploadProgress !== null && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between text-xs font-medium text-[var(--foreground-secondary)]">
|
||||||
|
<span>上传进度</span>
|
||||||
|
<span>{uploadProgress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-2 w-full overflow-hidden rounded-full bg-[var(--surface-muted)]">
|
||||||
|
<div
|
||||||
|
className="h-full rounded-full bg-[var(--brand-primary)] transition-all duration-300 ease-out"
|
||||||
|
style={{ width: `${uploadProgress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{errorMessage ? (
|
||||||
|
<p className="text-sm text-[var(--status-danger-foreground)]">
|
||||||
|
{errorMessage}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</AppModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function PagesProjectEditModal({
|
function PagesProjectEditModal({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
|
|||||||
Reference in New Issue
Block a user