mirror of
https://github.com/langgenius/dify.git
synced 2026-09-04 07:53:20 +08:00
Reintroduce the nullable api_tokens.dataset_id column (dropped in 2e9819ca5b28) so dataset API keys can opt into per-knowledge-base scoping: - NULL dataset_id keeps today's workspace-wide behavior, so every existing key and the existing /datasets/api-keys create route are unchanged. - validate_dataset_token rejects a bound key for any other dataset, and for endpoints that carry no dataset id (e.g. list-all), with 403. - CachedApiToken carries dataset_id with a None default so cache entries written before deploy keep deserializing. - The per-dataset console routes in apikey.py (previously dead code that 500ed on a missing ApiToken.dataset_id) now create bound keys; their list returns bound keys plus workspace keys so the dataset page shows the full access picture. - Frontend: the knowledge base API access popover gains an API keys entry; the secret key modal accepts datasetId, shows a scope column, and offers a workspace / this-knowledge-base scope choice on create. New strings are localized for all 23 locales.
110 lines
4.2 KiB
TypeScript
110 lines
4.2 KiB
TypeScript
import { cn } from '@langgenius/dify-ui/cn'
|
|
import { PopoverClose } from '@langgenius/dify-ui/popover'
|
|
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
|
import { Switch } from '@langgenius/dify-ui/switch'
|
|
import { RiArrowRightUpLine, RiBookOpenLine, RiKey2Line } from '@remixicon/react'
|
|
import * as React from 'react'
|
|
import { useCallback } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useSelector as useAppContextSelector } from '@/context/app-context'
|
|
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
|
import { useDatasetApiAccessUrl } from '@/hooks/use-api-access-url'
|
|
import Link from '@/next/link'
|
|
import { useDisableDatasetServiceApi, useEnableDatasetServiceApi } from '@/service/knowledge/use-dataset'
|
|
|
|
type CardProps = {
|
|
apiEnabled: boolean
|
|
onOpenSecretKeyModal: () => void
|
|
}
|
|
|
|
const Card = ({
|
|
apiEnabled,
|
|
onOpenSecretKeyModal,
|
|
}: CardProps) => {
|
|
const { t } = useTranslation()
|
|
const datasetId = useDatasetDetailContextWithSelector(state => state.dataset?.id)
|
|
const mutateDatasetRes = useDatasetDetailContextWithSelector(state => state.mutateDatasetRes)
|
|
const { mutateAsync: enableDatasetServiceApi } = useEnableDatasetServiceApi()
|
|
const { mutateAsync: disableDatasetServiceApi } = useDisableDatasetServiceApi()
|
|
|
|
const isCurrentWorkspaceManager = useAppContextSelector(state => state.isCurrentWorkspaceManager)
|
|
|
|
const apiReferenceUrl = useDatasetApiAccessUrl()
|
|
|
|
const onToggle = useCallback(async (state: boolean) => {
|
|
let result: 'success' | 'fail'
|
|
if (state)
|
|
result = (await enableDatasetServiceApi(datasetId ?? '')).result
|
|
else
|
|
result = (await disableDatasetServiceApi(datasetId ?? '')).result
|
|
if (result === 'success')
|
|
mutateDatasetRes?.()
|
|
}, [datasetId, enableDatasetServiceApi, mutateDatasetRes, disableDatasetServiceApi])
|
|
|
|
return (
|
|
<div className="w-[208px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg">
|
|
<div className="p-1">
|
|
<div className="p-2">
|
|
<div className="mb-1.5 flex justify-between">
|
|
<div className="flex items-center gap-1">
|
|
<StatusDot
|
|
className="shrink-0"
|
|
status={apiEnabled ? 'success' : 'warning'}
|
|
/>
|
|
<div
|
|
className={cn(
|
|
'system-xs-semibold-uppercase',
|
|
apiEnabled ? 'text-text-success' : 'text-text-warning',
|
|
)}
|
|
>
|
|
{apiEnabled
|
|
? t('serviceApi.enabled', { ns: 'dataset' })
|
|
: t('serviceApi.disabled', { ns: 'dataset' })}
|
|
</div>
|
|
</div>
|
|
<Switch
|
|
checked={apiEnabled}
|
|
onCheckedChange={onToggle}
|
|
disabled={!isCurrentWorkspaceManager}
|
|
/>
|
|
</div>
|
|
<div className="system-xs-regular text-text-tertiary">
|
|
{t('appMenus.apiAccessTip', { ns: 'common' })}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="h-px bg-divider-subtle"></div>
|
|
<div className="p-1">
|
|
<PopoverClose
|
|
render={(
|
|
<button
|
|
type="button"
|
|
className="flex h-8 w-full items-center space-x-[7px] rounded-lg border-none bg-transparent px-2 text-left text-text-tertiary hover:bg-state-base-hover"
|
|
onClick={onOpenSecretKeyModal}
|
|
>
|
|
<RiKey2Line className="size-3.5 shrink-0" />
|
|
<div className="grow truncate system-sm-regular">
|
|
{t('serviceApi.card.apiKey', { ns: 'dataset' })}
|
|
</div>
|
|
</button>
|
|
)}
|
|
/>
|
|
<Link
|
|
href={apiReferenceUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex h-8 items-center space-x-[7px] rounded-lg px-2 text-text-tertiary hover:bg-state-base-hover"
|
|
>
|
|
<RiBookOpenLine className="size-3.5 shrink-0" />
|
|
<div className="grow truncate system-sm-regular">
|
|
{t('overview.apiInfo.doc', { ns: 'appOverview' })}
|
|
</div>
|
|
<RiArrowRightUpLine className="size-3.5 shrink-0" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default React.memo(Card)
|