feat: integrate GitHub API for plugin version check and add access token support

This commit is contained in:
twwu 2024-11-05 16:25:20 +08:00
parent 52268460a1
commit 0b90625e57
10 changed files with 181 additions and 37 deletions

View File

@ -29,3 +29,6 @@ NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS=60000
# CSP https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
NEXT_PUBLIC_CSP_WHITELIST=
# Github Access Token, used for invoking Github API
NEXT_PUBLIC_GITHUB_ACCESS_TOKEN=

View File

@ -1,15 +1,25 @@
import { useState } from 'react'
import Toast from '@/app/components/base/toast'
import { uploadGitHub } from '@/service/plugins'
import { Octokit } from '@octokit/core'
import { GITHUB_ACCESS_TOKEN } from '@/config'
export const useGitHubReleases = () => {
const fetchReleases = async (owner: string, repo: string, setReleases: (releases: any) => void) => {
const fetchReleases = async (owner: string, repo: string) => {
try {
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases`)
if (!res.ok) throw new Error('Failed to fetch releases')
const data = await res.json()
const octokit = new Octokit({
auth: GITHUB_ACCESS_TOKEN,
})
const res = await octokit.request('GET /repos/{owner}/{repo}/releases', {
owner,
repo,
headers: {
'X-GitHub-Api-Version': '2022-11-28',
},
})
if (res.status !== 200) throw new Error('Failed to fetch releases')
const formattedReleases = data.map((release: any) => ({
const formattedReleases = res.data.map((release: any) => ({
tag_name: release.tag_name,
assets: release.assets.map((asset: any) => ({
browser_download_url: asset.browser_download_url,
@ -17,13 +27,14 @@ export const useGitHubReleases = () => {
})),
}))
setReleases(formattedReleases)
return formattedReleases
}
catch (error) {
Toast.notify({
type: 'error',
message: 'Failed to fetch repository releases',
})
return []
}
}

View File

@ -64,13 +64,12 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ onClose }) => {
})
break
}
await fetchReleases(owner, repo, (fetchedReleases) => {
setState(prevState => ({
...prevState,
releases: fetchedReleases,
step: InstallStepFromGitHub.selectPackage,
}))
})
const fetchedReleases = await fetchReleases(owner, repo)
setState(prevState => ({
...prevState,
releases: fetchedReleases,
step: InstallStepFromGitHub.selectPackage,
}))
break
}
case InstallStepFromGitHub.selectPackage: {

View File

@ -10,13 +10,17 @@ import ActionButton from '../../base/action-button'
import Tooltip from '../../base/tooltip'
import Confirm from '../../base/confirm'
import { uninstallPlugin } from '@/service/plugins'
import { usePluginPageContext } from '../plugin-page/context'
import { useGitHubReleases } from '../install-plugin/hooks'
import { compareVersion, getLatestVersion } from '@/utils/semver'
import Toast from '@/app/components/base/toast'
const i18nPrefix = 'plugin.action'
type Props = {
pluginId: string
author: string
installationId: string
pluginName: string
version: string
usedInApps: number
isShowFetchNewVersion: boolean
isShowInfo: boolean
@ -25,8 +29,10 @@ type Props = {
meta?: MetaData
}
const Action: FC<Props> = ({
pluginId,
author,
installationId,
pluginName,
version,
isShowFetchNewVersion,
isShowInfo,
isShowDelete,
@ -38,13 +44,35 @@ const Action: FC<Props> = ({
setTrue: showPluginInfo,
setFalse: hidePluginInfo,
}] = useBoolean(false)
const mutateInstalledPluginList = usePluginPageContext(v => v.mutateInstalledPluginList)
const [deleting, {
setTrue: showDeleting,
setFalse: hideDeleting,
}] = useBoolean(false)
const { fetchReleases } = useGitHubReleases()
const handleFetchNewVersion = () => { }
const handleFetchNewVersion = async () => {
try {
const fetchedReleases = await fetchReleases(author, pluginName)
const versions = fetchedReleases.map(release => release.tag_name)
const latestVersion = getLatestVersion(versions)
if (compareVersion(latestVersion, version) === 1) {
// todo: open plugin updating modal
console.log('New version available:', latestVersion)
}
else {
Toast.notify({
type: 'info',
message: 'No new version available',
})
}
}
catch {
Toast.notify({
type: 'error',
message: 'Failed to compare versions',
})
}
}
const [isShowDeleteConfirm, {
setTrue: showDeleteConfirm,
@ -53,14 +81,13 @@ const Action: FC<Props> = ({
const handleDelete = useCallback(async () => {
showDeleting()
const res = await uninstallPlugin(pluginId)
const res = await uninstallPlugin(installationId)
hideDeleting()
if (res.success) {
hideDeleteConfirm()
mutateInstalledPluginList()
onDelete()
}
}, [pluginId, onDelete])
}, [installationId])
return (
<div className='flex space-x-1'>
{/* Only plugin installed from GitHub need to check if it's the new version */}

View File

@ -37,6 +37,7 @@ const PluginItem: FC<Props> = ({
const { t } = useTranslation()
const currentPluginDetail = usePluginPageContext(v => v.currentPluginDetail)
const setCurrentPluginDetail = usePluginPageContext(v => v.setCurrentPluginDetail)
const mutateInstalledPluginList = usePluginPageContext(v => v.mutateInstalledPluginList)
const {
source,
@ -44,16 +45,10 @@ const PluginItem: FC<Props> = ({
installation_id,
endpoints_active,
meta,
version,
latest_version,
plugin_id,
version,
} = plugin
const { category, author, name, label, description, icon, verified } = plugin.declaration
// Only plugin installed from GitHub need to check if it's the new version
// todo check version manually
const hasNewVersion = useMemo(() => {
return source === PluginSource.github && latest_version !== version
}, [source, latest_version, version])
const orgName = useMemo(() => {
return [PluginSource.github, PluginSource.marketplace].includes(source) ? author : ''
@ -79,6 +74,7 @@ const PluginItem: FC<Props> = ({
<div className="flex">
<div className='flex items-center justify-center w-10 h-10 overflow-hidden border-components-panel-border-subtle border-[1px] rounded-xl'>
<img
className='w-full h-full'
src={`${API_PREFIX}/workspaces/current/plugin/icon?tenant_id=${tenant_id}&filename=${icon}`}
alt={`plugin-${installation_id}-logo`}
/>
@ -87,20 +83,24 @@ const PluginItem: FC<Props> = ({
<div className="flex items-center h-5">
<Title title={label[tLocale]} />
{verified && <RiVerifiedBadgeLine className="shrink-0 ml-0.5 w-4 h-4 text-text-accent" />}
<Badge className='ml-1' text={plugin.version} hasRedCornerMark={hasNewVersion} />
<Badge className='ml-1' text={plugin.version} />
</div>
<div className='flex items-center justify-between'>
<Description text={description[tLocale]} descriptionLineRows={1}></Description>
<div onClick={e => e.stopPropagation()}>
<Action
pluginId={installation_id}
pluginName={label[tLocale]}
installationId={installation_id}
author={author}
pluginName={name}
version={version}
usedInApps={5}
isShowFetchNewVersion={hasNewVersion}
isShowFetchNewVersion={source === PluginSource.github}
isShowInfo={source === PluginSource.github}
isShowDelete
meta={meta}
onDelete={() => {}}
onDelete={() => {
mutateInstalledPluginList()
}}
/>
</div>
</div>

View File

@ -45,6 +45,7 @@ const LocaleLayout = ({
data-public-maintenance-notice={process.env.NEXT_PUBLIC_MAINTENANCE_NOTICE}
data-public-site-about={process.env.NEXT_PUBLIC_SITE_ABOUT}
data-public-text-generation-timeout-ms={process.env.NEXT_PUBLIC_TEXT_GENERATION_TIMEOUT_MS}
data-public-github-access-token={process.env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN}
>
<BrowserInitor>
<SentryInitor>

View File

@ -271,3 +271,5 @@ else if (globalThis.document?.body?.getAttribute('data-public-text-generation-ti
export const TEXT_GENERATION_TIMEOUT_MS = textGenerationTimeoutMs
export const DISABLE_UPLOAD_IMAGE_AS_ICON = process.env.NEXT_PUBLIC_DISABLE_UPLOAD_IMAGE_AS_ICON === 'true'
export const GITHUB_ACCESS_TOKEN = process.env.NEXT_PUBLIC_GITHUB_ACCESS_TOKEN || globalThis.document?.body?.getAttribute('data-public-github-access-token') || ''

View File

@ -37,6 +37,7 @@
"@mdx-js/react": "^2.3.0",
"@monaco-editor/react": "^4.6.0",
"@next/mdx": "^14.0.4",
"@octokit/core": "^6.1.2",
"@remixicon/react": "^4.3.0",
"@sentry/react": "^7.54.0",
"@sentry/utils": "^7.54.0",
@ -98,6 +99,7 @@
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"scheduler": "^0.23.0",
"semver": "^7.6.3",
"server-only": "^0.0.1",
"sharp": "^0.33.5",
"sortablejs": "^1.15.3",
@ -144,6 +146,7 @@
"@types/react-window": "^1.8.8",
"@types/react-window-infinite-loader": "^1.0.9",
"@types/recordrtc": "^5.6.14",
"@types/semver": "^7.5.8",
"@types/sortablejs": "^1.15.1",
"@types/uuid": "^10.0.0",
"autoprefixer": "^10.4.14",
@ -153,9 +156,9 @@
"eslint": "^9.13.0",
"eslint-config-next": "^15.0.0",
"eslint-plugin-react-hooks": "^5.0.0",
"husky": "^9.1.6",
"eslint-plugin-react-refresh": "^0.4.13",
"eslint-plugin-storybook": "^0.10.1",
"husky": "^9.1.6",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^15.2.10",

View File

@ -52,6 +52,9 @@ importers:
'@next/mdx':
specifier: ^14.0.4
version: 14.2.15(@mdx-js/loader@2.3.0(webpack@5.95.0(esbuild@0.23.1)(uglify-js@3.19.3)))(@mdx-js/react@2.3.0(react@18.2.0))
'@octokit/core':
specifier: ^6.1.2
version: 6.1.2
'@remixicon/react':
specifier: ^4.3.0
version: 4.3.0(react@18.2.0)
@ -235,6 +238,9 @@ importers:
scheduler:
specifier: ^0.23.0
version: 0.23.2
semver:
specifier: ^7.6.3
version: 7.6.3
server-only:
specifier: ^0.0.1
version: 0.0.1
@ -368,6 +374,9 @@ importers:
'@types/recordrtc':
specifier: ^5.6.14
version: 5.6.14
'@types/semver':
specifier: ^7.5.8
version: 7.5.8
'@types/sortablejs':
specifier: ^1.15.1
version: 1.15.8
@ -1871,6 +1880,36 @@ packages:
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
engines: {node: '>=12.4.0'}
'@octokit/auth-token@5.1.1':
resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==}
engines: {node: '>= 18'}
'@octokit/core@6.1.2':
resolution: {integrity: sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==}
engines: {node: '>= 18'}
'@octokit/endpoint@10.1.1':
resolution: {integrity: sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==}
engines: {node: '>= 18'}
'@octokit/graphql@8.1.1':
resolution: {integrity: sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==}
engines: {node: '>= 18'}
'@octokit/openapi-types@22.2.0':
resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==}
'@octokit/request-error@6.1.5':
resolution: {integrity: sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==}
engines: {node: '>= 18'}
'@octokit/request@9.1.3':
resolution: {integrity: sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==}
engines: {node: '>= 18'}
'@octokit/types@13.6.1':
resolution: {integrity: sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==}
'@parcel/watcher-android-arm64@2.4.1':
resolution: {integrity: sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==}
engines: {node: '>= 10.0.0'}
@ -3158,6 +3197,9 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
before-after-hook@3.0.2:
resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==}
better-opn@3.0.2:
resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
engines: {node: '>=12.0.0'}
@ -7806,6 +7848,9 @@ packages:
unist-util-visit@5.0.0:
resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==}
universal-user-agent@7.0.2:
resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==}
universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
@ -9925,6 +9970,46 @@ snapshots:
'@nolyfill/is-core-module@1.0.39': {}
'@octokit/auth-token@5.1.1': {}
'@octokit/core@6.1.2':
dependencies:
'@octokit/auth-token': 5.1.1
'@octokit/graphql': 8.1.1
'@octokit/request': 9.1.3
'@octokit/request-error': 6.1.5
'@octokit/types': 13.6.1
before-after-hook: 3.0.2
universal-user-agent: 7.0.2
'@octokit/endpoint@10.1.1':
dependencies:
'@octokit/types': 13.6.1
universal-user-agent: 7.0.2
'@octokit/graphql@8.1.1':
dependencies:
'@octokit/request': 9.1.3
'@octokit/types': 13.6.1
universal-user-agent: 7.0.2
'@octokit/openapi-types@22.2.0': {}
'@octokit/request-error@6.1.5':
dependencies:
'@octokit/types': 13.6.1
'@octokit/request@9.1.3':
dependencies:
'@octokit/endpoint': 10.1.1
'@octokit/request-error': 6.1.5
'@octokit/types': 13.6.1
universal-user-agent: 7.0.2
'@octokit/types@13.6.1':
dependencies:
'@octokit/openapi-types': 22.2.0
'@parcel/watcher-android-arm64@2.4.1':
optional: true
@ -11618,6 +11703,8 @@ snapshots:
base64-js@1.5.1: {}
before-after-hook@3.0.2: {}
better-opn@3.0.2:
dependencies:
open: 8.4.2
@ -12757,7 +12844,7 @@ snapshots:
debug: 4.3.7
enhanced-resolve: 5.17.1
eslint: 9.13.0(jiti@1.21.6)
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@1.21.6))(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@1.21.6))(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import-x@4.3.1(eslint@9.13.0(jiti@1.21.6))(typescript@4.9.5))(eslint-plugin-import@2.31.0)(eslint@9.13.0(jiti@1.21.6)))(eslint@9.13.0(jiti@1.21.6))
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@1.21.6))(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.13.0(jiti@1.21.6))
fast-glob: 3.3.2
get-tsconfig: 4.8.1
is-bun-module: 1.2.1
@ -12775,7 +12862,7 @@ snapshots:
dependencies:
eslint: 9.13.0(jiti@1.21.6)
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@1.21.6))(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@1.21.6))(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import-x@4.3.1(eslint@9.13.0(jiti@1.21.6))(typescript@4.9.5))(eslint-plugin-import@2.31.0)(eslint@9.13.0(jiti@1.21.6)))(eslint@9.13.0(jiti@1.21.6)):
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@1.21.6))(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.13.0(jiti@1.21.6)):
dependencies:
debug: 3.2.7
optionalDependencies:
@ -12831,7 +12918,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.13.0(jiti@1.21.6)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@1.21.6))(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@1.21.6))(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import-x@4.3.1(eslint@9.13.0(jiti@1.21.6))(typescript@4.9.5))(eslint-plugin-import@2.31.0)(eslint@9.13.0(jiti@1.21.6)))(eslint@9.13.0(jiti@1.21.6))
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.11.0(eslint@9.13.0(jiti@1.21.6))(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@9.13.0(jiti@1.21.6))
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@ -17577,6 +17664,8 @@ snapshots:
unist-util-is: 6.0.0
unist-util-visit-parents: 6.0.1
universal-user-agent@7.0.2: {}
universalify@0.2.0: {}
universalify@2.0.1: {}

9
web/utils/semver.ts Normal file
View File

@ -0,0 +1,9 @@
import semver from 'semver'
export const getLatestVersion = (versionList: string[]) => {
return semver.rsort(versionList)[0]
}
export const compareVersion = (v1: string, v2: string) => {
return semver.compare(v1, v2)
}