From d40c3960e98a7c0adc55356dcf08e36c80234c30 Mon Sep 17 00:00:00 2001 From: Sylvan <60059315+wblxxx@users.noreply.github.com> Date: Sat, 3 Aug 2024 14:01:35 +0800 Subject: [PATCH] feat:Added functionality to retrieve and save GitHub token (#839) * feat:Added functionality to retrieve and save GitHub token (#812) Signed-off-by: wblx <904653630@qq.com> * solved all the problems Signed-off-by: wblx <904653630@qq.com> * sync * Resolve inconsistent margins Signed-off-by: wblx <904653630@qq.com> * response problem Signed-off-by: wblx <904653630@qq.com> * duplicated options Signed-off-by: wblx <904653630@qq.com> * chore: run prettier to format Signed-off-by: Harry --------- Signed-off-by: wblx <904653630@qq.com> Signed-off-by: Harry --- package.json | 2 +- src/api/githubApi.ts | 19 +++ src/helpers/github-token.ts | 7 + src/locales/en/translation.json | 26 +++- src/locales/zh_CN/translation.json | 26 +++- src/pages/Options/Options.css | 56 ++++++++ src/pages/Options/Options.tsx | 22 ++- src/pages/Options/components/GitHubToken.tsx | 134 +++++++++++++++++++ yarn.lock | 70 +++++----- 9 files changed, 320 insertions(+), 42 deletions(-) create mode 100644 src/api/githubApi.ts create mode 100644 src/helpers/github-token.ts create mode 100644 src/pages/Options/components/GitHubToken.tsx diff --git a/package.json b/package.json index 67dd7aaf..451ed7f8 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "mini-css-extract-plugin": "^2.7.2", "mkdirp": "^1.0.4", "prettier": "^3.3.3", - "pretty-quick": "^3.1.3", + "pretty-quick": "^4.0.0", "querystring": "^0.2.1", "sass": "^1.52.1", "sass-loader": "^12.4.0", diff --git a/src/api/githubApi.ts b/src/api/githubApi.ts new file mode 100644 index 00000000..791a9a00 --- /dev/null +++ b/src/api/githubApi.ts @@ -0,0 +1,19 @@ +import { getToken, saveToken } from '../helpers/github-token'; + +export const githubRequest = async (endpoint: string, options: RequestInit = {}): Promise => { + const token = getToken(); + if (!token) { + return null; + } + + try { + const response = await fetch(`https://api.github.com${endpoint}`, { + ...options, + }); + return response.json(); + } catch (error) { + return null; + } +}; + +export { saveToken, getToken }; diff --git a/src/helpers/github-token.ts b/src/helpers/github-token.ts new file mode 100644 index 00000000..3d649692 --- /dev/null +++ b/src/helpers/github-token.ts @@ -0,0 +1,7 @@ +export const saveToken = (token: string) => { + localStorage.setItem('github_token', token); +}; + +export const getToken = (): string => { + return localStorage.getItem('github_token') || ''; +}; diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index aa96bcb5..94662298 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -72,5 +72,29 @@ "activity_icon": "activity", "openrank_icon": "openrank", "contributors_participants_icon": "contributors and participants", - "merged_lines_icon": "code lines change" + "merged_lines_icon": "code lines change", + "github_token_configuration": "GitHub Token Configuration", + "github_token_tooltip": "Enter your GitHub Token here for authentication.", + "github_token_description": "Providing a GitHub Token ensures that HyperCRX can securely and effectively access and operate on your GitHub data for personalized analysis.", + "github_token_how_to_generate": "How to generate a GitHub Token?", + "github_token_step1": "1. Log in to GitHub and go to Settings.", + "github_token_step2": "2. Select Developer settings.", + "github_token_step3": "3. Choose Personal access tokens, click Tokens(classic), then Generate a personal access token.", + "github_token_step4": "4. Set token details:", + "github_token_note": "Note", + "github_token_note_description": "A descriptive name, e.g., \"HyperCrx Token\".", + "github_token_expiration": "Expiration", + "github_token_expiration_description": "Choose the validity period.", + "github_token_scopes": "Scopes", + "github_token_scopes_description": "Select permission scopes, such as repo and workflow.", + "github_token_step5": "5. Click the Generate token button.", + "github_token_step6": "6. Copy the generated token and paste it into the input box below.", + "github_token_placeholder": "GitHub Token", + "github_token_save": "Save", + "github_token_edit": "Edit", + "github_token_test": "Test Token", + "github_token_error_empty": "Token cannot be empty", + "github_token_success_save": "Token saved successfully", + "github_token_success_valid": "Token is valid. Username: {{username}}", + "github_token_error_invalid": "Invalid token or request failed" } diff --git a/src/locales/zh_CN/translation.json b/src/locales/zh_CN/translation.json index 76745e0c..fd92e923 100644 --- a/src/locales/zh_CN/translation.json +++ b/src/locales/zh_CN/translation.json @@ -71,5 +71,29 @@ "activity_icon": "activity数", "openrank_icon": "openrank值", "contributors_participants_icon": "contributors和participants数", - "merged_lines_icon": "代码变化量" + "merged_lines_icon": "代码变化量", + "github_token_configuration": "GitHub 令牌配置", + "github_token_tooltip": "在此输入您的 GitHub 令牌以进行身份验证。", + "github_token_description": "提供 GitHub 令牌可确保 HyperCRX 能够安全有效地访问和操作您的 GitHub 数据以进行个性化分析。", + "github_token_how_to_generate": "如何生成 GitHub 令牌?", + "github_token_step1": "1. 登录 GitHub 并进入设置。", + "github_token_step2": "2. 选择开发者设置。", + "github_token_step3": "3. 选择个人访问令牌,Tokens(classic),然后生成个人访问令牌。", + "github_token_step4": "4. 设置令牌详细信息:", + "github_token_note": "注意", + "github_token_note_description": "描述性名称,例如“HyperCrx Token", + "github_token_expiration": "过期时间", + "github_token_expiration_description": "选择有效期。", + "github_token_scopes": "权限范围", + "github_token_scopes_description": "选择权限范围,例如 repo 和 workflow。", + "github_token_step5": "5. 点击生成令牌按钮。", + "github_token_step6": "6. 复制生成的令牌并将其粘贴到下面的输入框中。", + "github_token_placeholder": "GitHub 令牌", + "github_token_save": "保存", + "github_token_edit": "编辑", + "github_token_test": "测试令牌", + "github_token_error_empty": "令牌不能为空", + "github_token_success_save": "令牌保存成功", + "github_token_success_valid": "令牌有效。用户名:{{username}}", + "github_token_error_invalid": "令牌无效或请求失败" } diff --git a/src/pages/Options/Options.css b/src/pages/Options/Options.css index 7e71844c..ba26d071 100644 --- a/src/pages/Options/Options.css +++ b/src/pages/Options/Options.css @@ -58,3 +58,59 @@ li { a { color: blue; } + +.github-token-options { + width: 75vw; + min-width: 350px; + max-width: 600px; + background-color: white; + border: 1px solid #e1e4e8; + border-radius: 6px; + margin-top: -10px; +} + +.github-token-options .Box-header { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + padding: 0 25px; + margin: -1px -1px 0; + background-color: #242a2e; + color: white; + border: 1px solid #e1e4e8; + border-top-left-radius: 6px; + border-top-right-radius: 6px; +} + +.github-token-options .Box-title { + font-size: 18px; + font-weight: 600; + margin-right: 8px; +} + +.github-token-options p { + padding: 0 25px; +} + +.github-token-options input { + width: calc(100% - 50px); + padding: 8px; + margin: 10px 25px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.github-token-options button { + padding: 8px 16px; + margin: 0 25px 20px; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.github-token-options button:hover { + background-color: #0056b3; +} diff --git a/src/pages/Options/Options.tsx b/src/pages/Options/Options.tsx index b42adcaf..1e8f05c4 100644 --- a/src/pages/Options/Options.tsx +++ b/src/pages/Options/Options.tsx @@ -7,6 +7,8 @@ import TooltipTrigger from '../../components/TooltipTrigger'; import './Options.css'; import { useTranslation } from 'react-i18next'; import '../../helpers/i18n'; +import GitHubToken from './components/GitHubToken'; + const stacksStyleOptions = { headerStack: { paddingBottom: '10px', @@ -17,6 +19,9 @@ const stacksStyleOptions = { settingStack: { margin: '10px 25px', }, + tokenStack: { + margin: '10px 25px', + }, }; const Options = (): JSX.Element => { @@ -64,11 +69,7 @@ const Options = (): JSX.Element => { - + { + + {/* Add GitHubToken component */} + diff --git a/src/pages/Options/components/GitHubToken.tsx b/src/pages/Options/components/GitHubToken.tsx new file mode 100644 index 00000000..6e919ba2 --- /dev/null +++ b/src/pages/Options/components/GitHubToken.tsx @@ -0,0 +1,134 @@ +import React, { useState, useEffect, useRef } from 'react'; +import TooltipTrigger from '../../../components/TooltipTrigger'; +import { saveToken, getToken, githubRequest } from '../../../api/githubApi'; +import { message } from 'antd'; +import { useTranslation } from 'react-i18next'; + +const GitHubToken = () => { + const [token, setToken] = useState(''); + const [isCollapsed, setIsCollapsed] = useState(true); + const [isEditing, setIsEditing] = useState(false); + const { t } = useTranslation(); + const inputRef = useRef(null); + + useEffect(() => { + const storedToken = getToken(); + if (storedToken) { + setToken(storedToken); + } + }, []); + + const handleSave = () => { + if (!token.trim()) { + showMessage(t('github_token_error_empty'), 'error'); + return; + } + saveToken(token); + showMessage(t('github_token_success_save'), 'success'); + setIsEditing(false); + }; + + const handleEdit = () => { + setIsEditing(true); + }; + + const handleTestToken = async () => { + const userData = await githubRequest('/user', { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (userData === null || userData.message) { + showMessage(t('github_token_error_invalid'), 'error'); + } else { + showMessage(t('github_token_success_valid', { username: userData.login }), 'success'); + } + }; + + const obfuscateToken = (token: string): string => { + if (token.length <= 4) return token; + return `${token[0]}${'*'.repeat(token.length - 2)}${token[token.length - 1]}`; + }; + + const showMessage = (content: string, type: 'success' | 'error') => { + if (inputRef.current) { + const rect = inputRef.current.getBoundingClientRect(); + message.config({ + top: rect.top - 50, + duration: 2, + maxCount: 3, + }); + message[type](content); + } + }; + + return ( +
+
+

{t('github_token_configuration')}

+ +
+

{t('github_token_description')}

+
+
setIsCollapsed(!isCollapsed)} + style={{ cursor: 'pointer', display: 'flex', alignItems: 'center' }} + > +

{t('github_token_how_to_generate')}

+ {isCollapsed ? '▶' : '▼'} +
+ {!isCollapsed && ( +
+
    +
  1. {t('github_token_step1')}
  2. +
  3. {t('github_token_step2')}
  4. +
  5. {t('github_token_step3')}
  6. +
  7. + {t('github_token_step4')} +
      +
    • + {t('github_token_note')}: {t('github_token_note_description')} +
    • +
    • + {t('github_token_expiration')}: {t('github_token_expiration_description')} +
    • +
    • + {t('github_token_scopes')}: {t('github_token_scopes_description')} +
    • +
    +
  8. +
  9. {t('github_token_step5')}
  10. +
  11. {t('github_token_step6')}
  12. +
+
+ )} +
+
+
+ setToken(e.target.value)} + placeholder={t('github_token_placeholder')} + style={{ marginRight: '10px', flex: 1 }} + disabled={!isEditing} + /> + {isEditing ? ( + + ) : ( + + )} + +
+
+ ); +}; + +export default GitHubToken; diff --git a/yarn.lock b/yarn.lock index 80d897c7..a37fb169 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2507,7 +2507,7 @@ cross-fetch@4.0.0: dependencies: node-fetch "^2.6.12" -cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.3: +cross-spawn@^7.0.1, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2930,24 +2930,9 @@ events@^3.2.0: resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -execa@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" - integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== - dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" - -execa@^5.0.0: +execa@^5.0.0, execa@^5.1.1: version "5.1.1" - resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== dependencies: cross-spawn "^7.0.3" @@ -3099,7 +3084,7 @@ find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" -find-up@^4.0.0, find-up@^4.1.0: +find-up@^4.0.0: version "4.1.0" resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== @@ -3107,6 +3092,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + flat@^5.0.2: version "5.0.2" resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" @@ -3213,7 +3206,7 @@ get-pixels@^3.3.2: request "^2.44.0" through "^2.3.4" -get-stream@^5.0.0, get-stream@^5.1.0: +get-stream@^5.1.0: version "5.2.0" resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== @@ -3552,11 +3545,6 @@ http2-wrapper@^2.1.10: quick-lru "^5.1.1" resolve-alpn "^1.2.0" -human-signals@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" - integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== - human-signals@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" @@ -3962,6 +3950,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash-es@^4.17.21: version "4.17.21" resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" @@ -4313,7 +4308,7 @@ normalize-url@^8.0.0: resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz#9b7d96af9836577c58f5883e939365fa15623a4a" integrity sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w== -npm-run-path@^4.0.0, npm-run-path@^4.0.1: +npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -4371,7 +4366,7 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^5.1.0, onetime@^5.1.2: +onetime@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== @@ -4423,6 +4418,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" @@ -4643,13 +4645,13 @@ pretty-format@^27.0.0, pretty-format@^27.5.1: ansi-styles "^5.0.0" react-is "^17.0.1" -pretty-quick@^3.1.3: - version "3.3.1" - resolved "https://registry.npmjs.org/pretty-quick/-/pretty-quick-3.3.1.tgz#cfde97fec77a8d201a0e0c9c71d9990e12587ee2" - integrity sha512-3b36UXfYQ+IXXqex6mCca89jC8u0mYLqFAN5eTQKoXO6oCQYcIVYZEB/5AlBHI7JPYygReM2Vv6Vom/Gln7fBg== +pretty-quick@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/pretty-quick/-/pretty-quick-4.0.0.tgz#ea5cce85a5804bfbec7327b0e064509155d03f39" + integrity sha512-M+2MmeufXb/M7Xw3Afh1gxcYpj+sK0AxEfnfF958ktFeAyi5MsKY5brymVURQLgPLV1QaF5P4pb2oFJ54H3yzQ== dependencies: - execa "^4.1.0" - find-up "^4.1.0" + execa "^5.1.1" + find-up "^5.0.0" ignore "^5.3.0" mri "^1.2.0" picocolors "^1.0.0" @@ -5645,7 +5647,7 @@ side-channel@^1.0.4: get-intrinsic "^1.2.4" object-inspect "^1.13.1" -signal-exit@^3.0.2, signal-exit@^3.0.3: +signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==