Files
coze-studio/frontend/packages/studio/workspace/project-publish/src/hooks/use-publish-status.tsx

262 lines
7.9 KiB
TypeScript

/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect, useState } from 'react';
import { useRequest } from 'ahooks';
import {
ConnectorPublishStatus,
PublishRecordStatus,
type PublishRecordDetail,
} from '@coze-arch/idl/intelligence_api';
import { I18n, type I18nKeysNoOptionsType } from '@coze-arch/i18n';
import {
IconCozCheckMarkCircle,
IconCozClock,
IconCozCrossCircle,
} from '@coze-arch/coze-design/icons';
import { Modal, Select, Tag, type TagProps } from '@coze-arch/coze-design';
import { type OptionProps } from '@coze-arch/bot-semi/Select';
import { intelligenceApi } from '@coze-arch/bot-api';
import { EProjectPermission, useProjectAuth } from '@coze-common/auth';
import { isPublishFinish } from '../utils/is-publish-finish';
import { ProjectPublishProgress } from '../publish-progress';
enum PublishStatus {
Publishing,
Failed,
Success,
}
const PublishStatusMap: Record<
PublishStatus,
Pick<TagProps, 'prefixIcon' | 'color'> & { text: I18nKeysNoOptionsType }
> = {
[PublishStatus.Publishing]: {
prefixIcon: <IconCozClock />,
color: 'brand',
text: 'project_releasing',
},
[PublishStatus.Failed]: {
prefixIcon: <IconCozCrossCircle />,
color: 'red',
text: 'project_release_failed',
},
[PublishStatus.Success]: {
prefixIcon: <IconCozCheckMarkCircle />,
color: 'green',
text: 'project_release_success',
},
};
function toPublishStatus(record: PublishRecordDetail) {
const projectFailed =
record.publish_status === PublishRecordStatus.PackFailed ||
record.publish_status === PublishRecordStatus.AuditNotPass;
const connectorsFailed =
record.connector_publish_result?.some(
item => item.connector_publish_status === ConnectorPublishStatus.Failed,
) ?? false;
// The project itself failed, or some channels failed to publish - > overall failed
if (projectFailed || connectorsFailed) {
return PublishStatus.Failed;
}
const projectPublishing =
record.publish_status === PublishRecordStatus.Packing ||
record.publish_status === PublishRecordStatus.Auditing ||
record.publish_status === PublishRecordStatus.ConnectorPublishing;
const connectorsPublishing =
record.connector_publish_result?.some(
item =>
item.connector_publish_status === ConnectorPublishStatus.Default ||
item.connector_publish_status === ConnectorPublishStatus.Auditing,
) ?? false;
// The project itself is being released, or some channels are being released - > the overall release is in progress
if (projectPublishing || connectorsPublishing) {
return PublishStatus.Publishing;
}
return PublishStatus.Success;
}
export interface ProjectPublishStatusProps {
spaceId: string;
projectId: string;
defaultRecordID?: string;
}
/* eslint @coze-arch/max-line-per-function: ["error", {"max": 300}] */
export function usePublishStatus({
spaceId,
projectId,
defaultRecordID,
}: ProjectPublishStatusProps) {
const [status, setStatus] = useState<PublishStatus | undefined>();
const [latestRecord, setLatestRecord] = useState<PublishRecordDetail>();
const [recordList, setRecordList] = useState<OptionProps[]>([]);
const [selectedVersion, setSelectedVersion] = useState(defaultRecordID);
const [selectedRecord, setSelectedRecord] = useState<PublishRecordDetail>();
const [modalVisible, setModalVisible] = useState(false);
// Polling the latest release history until it stops when it is not in the "In Release" state
const latestRecordRequest = useRequest(
() => intelligenceApi.GetPublishRecordDetail({ project_id: projectId }),
{
manual: true,
pollingInterval: 5000,
pollingWhenHidden: false,
pollingErrorRetryCount: 3,
onSuccess: res => {
const record = res.data;
// Stop polling when no record is published
if (!record || typeof record.publish_status !== 'number') {
latestRecordRequest.cancel();
return;
}
setStatus(toPublishStatus(record));
setLatestRecord(record);
// After first requesting the latest release record, its version number is selected by default
if (!selectedVersion) {
setRecordList([
{ value: record.publish_record_id, label: record.version_number },
]);
setSelectedVersion(record.publish_record_id ?? '');
} else if (selectedVersion === record.publish_record_id) {
setSelectedRecord(record);
}
if (isPublishFinish(record)) {
latestRecordRequest.cancel();
}
},
},
);
// Get a list of publication records
const recordListRequest = useRequest(
() => intelligenceApi.GetPublishRecordList({ project_id: projectId }),
{
manual: true,
onSuccess: res => {
setRecordList(
res.data?.map(item => ({
value: item.publish_record_id,
label: item.version_number,
})) ?? [],
);
},
},
);
const hasPermission = useProjectAuth(
EProjectPermission.PUBLISH,
projectId,
spaceId,
);
// When the user has "publish" permission, start polling
useEffect(() => {
if (!hasPermission || defaultRecordID) {
return;
}
latestRecordRequest.run();
}, [hasPermission, defaultRecordID]);
// Manually request selected release records
const recordDetailRequest = useRequest(
(recordId: string) =>
intelligenceApi.GetPublishRecordDetail({
project_id: projectId,
publish_record_id: recordId,
}),
{
manual: true,
onSuccess: res => {
const record = res.data;
setSelectedRecord(record);
if (record?.publish_record_id === latestRecord?.publish_record_id) {
setLatestRecord(record);
}
},
},
);
const tagConfig = PublishStatusMap[status ?? PublishStatus.Failed];
const showingRecord = selectedRecord ?? latestRecord;
const open = async () => {
await recordListRequest.runAsync();
if (defaultRecordID) {
await changeVersion(defaultRecordID);
}
setModalVisible(true);
};
const close = () => {
setModalVisible(false);
};
const changeVersion = async (version: string) => {
setSelectedVersion(version);
await recordDetailRequest.run(version);
};
return {
latestVersion: latestRecord,
currentVersion: recordList.find(item => item.value === selectedVersion),
open,
close,
modal: (
<Modal
title={I18n.t('project_release_stage')}
visible={modalVisible}
footer={null}
onCancel={() => setModalVisible(false)}
>
<div className="flex flex-col gap-[16px]">
<Select
className="w-full"
optionList={recordList}
value={selectedVersion}
onSelect={version => {
if (typeof version === 'string') {
changeVersion(version);
}
}}
/>
{showingRecord ? (
<ProjectPublishProgress record={showingRecord} />
) : null}
</div>
</Modal>
),
tag: (
<Tag
size="mini"
prefixIcon={tagConfig.prefixIcon}
color={tagConfig.color}
// The Tag component defaults to display: inline-flex, and the outer span line-height > 1 will cause its height to be larger than the Tag itself
className="flex !px-[3px] font-medium"
>
{I18n.t(tagConfig.text)}
</Tag>
),
};
}