commit
b284ea8537
|
@ -0,0 +1,17 @@
|
|||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
name: Deploy
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Publish
|
||||
uses: cloudflare/wrangler-action@2.0.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
workingDirectory: 'frontend'
|
||||
command: pages publish --project-name=${{ secrets.CF_PROJECT_NAME }}
|
|
@ -31,7 +31,7 @@ up(){
|
|||
if screen -list | grep -q "$SCREEN_NAME_BACKEND"; then
|
||||
screen -S "$SCREEN_NAME_BACKEND" -X quit
|
||||
fi
|
||||
screen -dmS "$SCREEN_NAME_BACKEND" bash -c "cd ../ && poetry run python3 -m gfibot.backend.server --host 0.0.0.0 --port ${GFIBOT_BACKEND_PORT} --reload"
|
||||
screen -dmS "$SCREEN_NAME_BACKEND" bash -c "cd ../ && poetry run uvicorn gfibot.backend.server:app --host 0.0.0.0 --port ${GFIBOT_BACKEND_PORT} --reload --reload-dir gfibot/"
|
||||
|
||||
echo "[${COMPOSE_PROJECT_NAME}] Starting vite..."
|
||||
if screen -list | grep -q "$SCREEN_NAME_VITE"; then
|
||||
|
|
|
@ -6,9 +6,10 @@
|
|||
},
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"prettier",
|
||||
"plugin:react-svg/recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
"prettier"
|
||||
],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
|
@ -35,8 +36,12 @@
|
|||
"react/jsx-indent": ["warn", 2, { "indentLogicalExpressions": true }],
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-no-target-blank": "off",
|
||||
"no-unused-vars": ["warn", { "args": "none" }],
|
||||
"prefer-const": "warn",
|
||||
"typescript-eslint/ban-ts-comment": "off"
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"no-empty-function": "warn",
|
||||
"@typescript-eslint/no-empty-function": "warn",
|
||||
"no-unused-expressions": "warn",
|
||||
"no-unused-vars": "warn",
|
||||
"@typescript-eslint/no-empty-interface": "warn"
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
content="ML-powered 🤖 for finding and labeling good first issues in your GitHub project!"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<!--
|
||||
|
|
|
@ -27,14 +27,10 @@
|
|||
"dotenv-expand": "^5.1.0",
|
||||
"echarts": "^5.2.2",
|
||||
"echarts-for-react": "^3.0.2",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-react-app": "^7.0.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"gsap": "^3.9.1",
|
||||
"jest": "^27.4.3",
|
||||
"jest-watch-typeahead": "^1.0.0",
|
||||
"prettier": "^2.6.2",
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^17.0.2",
|
||||
"react-activation": "^0.10.2",
|
||||
|
@ -160,6 +156,10 @@
|
|||
"eslint-plugin-react": "^7.29.4",
|
||||
"eslint-plugin-react-hooks": "^4.5.0",
|
||||
"typescript": "^4.6.3",
|
||||
"vite": "^3.0.0"
|
||||
"vite": "^3.0.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-react-app": "^7.0.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"prettier": "^2.6.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,71 +1,79 @@
|
|||
import { asyncRequest, getBaseURL } from './query';
|
||||
import {
|
||||
GetRepoDetailedInfo,
|
||||
GFIInfo,
|
||||
GFIRepoConfig,
|
||||
GFIRepoInfo,
|
||||
GFITrainingSummary,
|
||||
GFIUserSearch,
|
||||
} from '../module/data/dataModel';
|
||||
import { store } from '../module/storage/configureStorage';
|
||||
import { convertFilter } from '../utils';
|
||||
import type { RequestParams } from './query';
|
||||
import { userInfo } from '../storage';
|
||||
|
||||
export const userInfo = () => {
|
||||
return [
|
||||
store.getState().loginReducer.hasLogin,
|
||||
store.getState().loginReducer.name,
|
||||
store.getState().loginReducer.loginName,
|
||||
store.getState().loginReducer.token,
|
||||
];
|
||||
import type {
|
||||
RepoSort,
|
||||
RepoBrief,
|
||||
RepoDetail,
|
||||
RepoGFIConfig,
|
||||
SearchedRepo,
|
||||
RepoUpdateConfig,
|
||||
UserQueryHistory,
|
||||
GFIInfo,
|
||||
GFITrainingSummary,
|
||||
GFIResponse,
|
||||
GFIFailure,
|
||||
} from '../model/api';
|
||||
|
||||
const requestGFI = async <T>(params: RequestParams) => {
|
||||
// if token exists, add token to headers
|
||||
const { githubToken } = userInfo();
|
||||
if (githubToken) params.headers = { Authorization: `token ${githubToken}` };
|
||||
const res = await asyncRequest<GFIResponse<T>>(params);
|
||||
if (!res) return undefined;
|
||||
if (200 <= res.code && res.code < 300 && res.result) {
|
||||
return res.result;
|
||||
} else if (typeof params.onError === 'function') {
|
||||
// normally when an error occurs, status code != 200
|
||||
// but in this case, we want to keep the compatibility
|
||||
params.onError(new Error(String(res.result)));
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getRepoNum = async (lang?: string) => {
|
||||
return await asyncRequest<number | undefined>({
|
||||
return await requestGFI<number>({
|
||||
url: '/api/repos/num',
|
||||
params: {
|
||||
lang,
|
||||
},
|
||||
baseURL: getBaseURL(),
|
||||
params: { lang },
|
||||
});
|
||||
};
|
||||
|
||||
export const getPagedRepoDetailedInfo = async (
|
||||
beginIdx: string | number,
|
||||
capacity: string | number,
|
||||
start: string | number,
|
||||
length: string | number,
|
||||
lang?: string,
|
||||
filter?: string
|
||||
filter?: RepoSort
|
||||
) => {
|
||||
return await asyncRequest<GetRepoDetailedInfo>({
|
||||
return await requestGFI<RepoDetail[]>({
|
||||
url: '/api/repos/info/',
|
||||
params: {
|
||||
start: beginIdx,
|
||||
length: capacity,
|
||||
lang,
|
||||
filter: convertFilter(filter),
|
||||
},
|
||||
params: { start, length, lang, filter },
|
||||
baseURL: getBaseURL(),
|
||||
});
|
||||
};
|
||||
|
||||
export const getPagedRepoBrief = async (
|
||||
start: number,
|
||||
length: number,
|
||||
lang?: string,
|
||||
filter?: RepoSort
|
||||
) =>
|
||||
await requestGFI<RepoBrief[]>({
|
||||
url: '/api/repos/info/paged',
|
||||
params: { start, length, lang, filter },
|
||||
});
|
||||
|
||||
export const getRepoDetailedInfo = async (name: string, owner: string) => {
|
||||
return await asyncRequest<GetRepoDetailedInfo>({
|
||||
return await requestGFI<RepoDetail>({
|
||||
url: '/api/repos/info/detail',
|
||||
params: {
|
||||
name,
|
||||
owner,
|
||||
},
|
||||
baseURL: getBaseURL(),
|
||||
params: { name, owner },
|
||||
});
|
||||
};
|
||||
|
||||
export const getRepoInfo = async (name: string, owner: string) => {
|
||||
return await asyncRequest<GFIRepoInfo>({
|
||||
return await requestGFI<RepoBrief>({
|
||||
url: '/api/repos/info',
|
||||
params: {
|
||||
name,
|
||||
owner,
|
||||
},
|
||||
baseURL: getBaseURL(),
|
||||
params: { name, owner },
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -73,99 +81,88 @@ export const searchRepoInfoByNameOrURL = async (
|
|||
repoName?: string,
|
||||
repoURL?: string
|
||||
) => {
|
||||
const [hasLogin, _, userLogin] = userInfo();
|
||||
return await asyncRequest<[GFIRepoInfo]>({
|
||||
const { githubLogin } = userInfo();
|
||||
return await requestGFI<[RepoBrief]>({
|
||||
url: '/api/repos/info/search',
|
||||
params: {
|
||||
repo: repoName,
|
||||
url: repoURL,
|
||||
user: userLogin,
|
||||
user: githubLogin,
|
||||
},
|
||||
baseURL: getBaseURL(),
|
||||
});
|
||||
};
|
||||
|
||||
export const getGFIByRepoName = async (repoName: string, repoOwner: string) => {
|
||||
return await asyncRequest<GFIInfo>({
|
||||
export const getGFIByRepoName = async (
|
||||
name: string,
|
||||
owner: string,
|
||||
start?: number,
|
||||
length?: number
|
||||
) =>
|
||||
await requestGFI<GFIInfo[]>({
|
||||
url: '/api/issue/gfi',
|
||||
params: {
|
||||
repo: repoName,
|
||||
owner: repoOwner,
|
||||
},
|
||||
baseURL: getBaseURL(),
|
||||
params: { owner, repo: name, start, length },
|
||||
});
|
||||
};
|
||||
|
||||
export const getGFINum = async (repoName?: string, repoOwner?: string) => {
|
||||
return await asyncRequest<number | undefined>({
|
||||
return await requestGFI<number | undefined>({
|
||||
url: '/api/issue/gfi/num',
|
||||
params: {
|
||||
repo: repoName,
|
||||
name: repoName,
|
||||
owner: repoOwner,
|
||||
},
|
||||
baseURL: getBaseURL(),
|
||||
});
|
||||
};
|
||||
|
||||
export const getLanguageTags = async () => {
|
||||
return await asyncRequest<string[]>({
|
||||
return await requestGFI<string[]>({
|
||||
url: '/api/repos/language',
|
||||
baseURL: getBaseURL(),
|
||||
});
|
||||
};
|
||||
|
||||
export const addRepoToGFIBot = async (repoName: string, repoOwner: string) => {
|
||||
const [hasLogin, _, loginName] = userInfo();
|
||||
return await asyncRequest<any>({
|
||||
const { githubLogin } = userInfo();
|
||||
return await requestGFI<any>({
|
||||
method: 'POST',
|
||||
url: '/api/repos/add',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
user: loginName,
|
||||
user: githubLogin,
|
||||
repo: repoName,
|
||||
owner: repoOwner,
|
||||
},
|
||||
baseURL: getBaseURL(),
|
||||
});
|
||||
};
|
||||
|
||||
export const getAddRepoHistory = async (filter?: string) => {
|
||||
const [_, __, loginName] = userInfo();
|
||||
return await asyncRequest<{
|
||||
nums?: number;
|
||||
queries: GFIRepoInfo[];
|
||||
finished_queries?: GFIRepoInfo[];
|
||||
}>({
|
||||
export const getAddRepoHistory = async (filter?: RepoSort) => {
|
||||
const { githubLogin } = userInfo();
|
||||
return await requestGFI<UserQueryHistory>({
|
||||
url: '/api/user/queries',
|
||||
params: {
|
||||
user: loginName,
|
||||
filter: convertFilter(filter),
|
||||
user: githubLogin,
|
||||
filter: filter,
|
||||
},
|
||||
baseURL: getBaseURL(),
|
||||
});
|
||||
};
|
||||
|
||||
export const getTrainingSummary = async (name?: string, owner?: string) => {
|
||||
return await asyncRequest<GFITrainingSummary[]>({
|
||||
return await requestGFI<GFITrainingSummary[]>({
|
||||
url: '/api/model/training/result',
|
||||
params: {
|
||||
name,
|
||||
owner,
|
||||
},
|
||||
baseURL: getBaseURL(),
|
||||
});
|
||||
};
|
||||
|
||||
export const getUserSearches = async () => {
|
||||
const [_, __, githubLogin] = userInfo();
|
||||
return await asyncRequest<GFIUserSearch[]>({
|
||||
const { githubLogin } = userInfo();
|
||||
return await requestGFI<SearchedRepo[]>({
|
||||
url: '/api/user/searches',
|
||||
params: {
|
||||
user: githubLogin,
|
||||
},
|
||||
baseURL: getBaseURL(),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -174,8 +171,8 @@ export const deleteUserSearch = async (
|
|||
owner: string,
|
||||
id: number
|
||||
) => {
|
||||
const [_, __, githubLogin] = userInfo();
|
||||
return await asyncRequest<GFIUserSearch[]>({
|
||||
const { githubLogin } = userInfo();
|
||||
return await requestGFI<SearchedRepo[]>({
|
||||
method: 'DELETE',
|
||||
url: '/api/user/searches',
|
||||
params: {
|
||||
|
@ -184,16 +181,15 @@ export const deleteUserSearch = async (
|
|||
owner,
|
||||
id,
|
||||
},
|
||||
baseURL: getBaseURL(),
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteRepoQuery = async (name: string, owner: string) => {
|
||||
const [_, __, githubLogin] = userInfo();
|
||||
return await asyncRequest<{
|
||||
const { githubLogin } = userInfo();
|
||||
return await requestGFI<{
|
||||
nums?: number;
|
||||
queries: GFIRepoInfo[];
|
||||
finished_queries?: GFIRepoInfo[];
|
||||
queries: RepoBrief[];
|
||||
finished_queries?: RepoBrief[];
|
||||
}>({
|
||||
method: 'DELETE',
|
||||
url: '/api/user/queries',
|
||||
|
@ -207,8 +203,8 @@ export const deleteRepoQuery = async (name: string, owner: string) => {
|
|||
};
|
||||
|
||||
export const updateRepoInfo = async (name: string, owner: string) => {
|
||||
const [_, __, githubLogin] = userInfo();
|
||||
return await asyncRequest<string>({
|
||||
const { githubLogin } = userInfo();
|
||||
return await requestGFI<string>({
|
||||
method: 'PUT',
|
||||
url: '/api/repos/update/',
|
||||
data: {
|
||||
|
@ -221,25 +217,24 @@ export const updateRepoInfo = async (name: string, owner: string) => {
|
|||
};
|
||||
|
||||
export const getRepoConfig = async (name: string, owner: string) => {
|
||||
const [_, __, githubLogin] = userInfo();
|
||||
return await asyncRequest<GFIRepoConfig>({
|
||||
const { githubLogin } = userInfo();
|
||||
return await requestGFI<RepoGFIConfig>({
|
||||
url: '/api/user/queries/config',
|
||||
params: {
|
||||
user: githubLogin,
|
||||
name,
|
||||
owner,
|
||||
},
|
||||
baseURL: getBaseURL(),
|
||||
});
|
||||
};
|
||||
|
||||
export const updateRepoConfig = async (
|
||||
name: string,
|
||||
owner: string,
|
||||
config: GFIRepoConfig
|
||||
config: RepoGFIConfig
|
||||
) => {
|
||||
const [_, __, githubLogin] = userInfo();
|
||||
return await asyncRequest<string>({
|
||||
const { githubLogin } = userInfo();
|
||||
return await requestGFI<string>({
|
||||
method: 'PUT',
|
||||
url: '/api/user/queries/config',
|
||||
params: {
|
||||
|
@ -247,12 +242,6 @@ export const updateRepoConfig = async (
|
|||
name,
|
||||
owner,
|
||||
},
|
||||
data: {
|
||||
newcomer_threshold: config.newcomer_threshold,
|
||||
gfi_threshold: config.gfi_threshold,
|
||||
need_comment: config.need_comment,
|
||||
issue_tag: config.issue_tag,
|
||||
},
|
||||
baseURL: getBaseURL(),
|
||||
data: config,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,15 +1,37 @@
|
|||
import { store } from '../module/storage/configureStorage';
|
||||
import { asyncRequest, getBaseURL } from './query';
|
||||
import { userInfo } from './api';
|
||||
import { asyncRequest, RequestParams } from './query';
|
||||
import { userInfo } from '../storage';
|
||||
import {
|
||||
GitHubIssueResponse,
|
||||
RepoPermissions,
|
||||
StandardHTTPResponse,
|
||||
} from '../module/data/dataModel';
|
||||
GitHubRepoPermissions,
|
||||
GitHubHTTPResponse,
|
||||
} from '../model/github';
|
||||
|
||||
export const requestGitHub = async <T>(params: RequestParams) => {
|
||||
// if token exists, add token to headers
|
||||
const { githubToken } = userInfo();
|
||||
if (githubToken) params.headers = { Authorization: `token ${githubToken}` };
|
||||
const res = await asyncRequest<GitHubHTTPResponse<T>>(params);
|
||||
if (!res) return undefined;
|
||||
if (res && !res.error) {
|
||||
return res.data ? res.data : res;
|
||||
} else if (typeof params.onError === 'function') {
|
||||
// normally when an error occurs, status code != 200
|
||||
// but in this case, we want to keep the compatibility
|
||||
params.onError(new Error(String(res.error)));
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** redirect to gh oauth login */
|
||||
const gitHubOAuthLogin = async () => {
|
||||
return await asyncRequest<string>({
|
||||
url: '/api/user/github/login',
|
||||
});
|
||||
};
|
||||
|
||||
export const gitHubLogin = () => {
|
||||
const [hasLogin, userName] = userInfo();
|
||||
if (hasLogin && userName !== undefined) {
|
||||
const { hasLogin, name } = userInfo();
|
||||
if (hasLogin && name !== undefined) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
@ -21,51 +43,32 @@ export const gitHubLogin = () => {
|
|||
};
|
||||
|
||||
export const checkGithubLogin = async () => {
|
||||
const userToken = store.getState().loginReducer.token;
|
||||
const userLoginName = store.getState().loginReducer.loginName;
|
||||
if (userToken) {
|
||||
const res = await asyncRequest<StandardHTTPResponse<any>>({
|
||||
url: `https://api.github.com/users/${userLoginName}`,
|
||||
headers: {
|
||||
Authorization: `token ${userToken}`,
|
||||
},
|
||||
customRequestResponse: false,
|
||||
const { githubLogin, githubToken } = userInfo();
|
||||
if (githubToken) {
|
||||
const res = await requestGitHub<any>({
|
||||
url: `https://api.github.com/users/${githubLogin}`,
|
||||
});
|
||||
if (res?.status === 200) {
|
||||
return true;
|
||||
}
|
||||
if (res) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const gitHubOAuthLogin = async () => {
|
||||
return await asyncRequest<string>({
|
||||
url: '/api/user/github/login',
|
||||
baseURL: getBaseURL(),
|
||||
});
|
||||
};
|
||||
|
||||
/** User must have write access */
|
||||
export const checkHasRepoPermissions = async (
|
||||
repoName: string,
|
||||
owner: string
|
||||
) => {
|
||||
const { hasLogin } = store.getState().loginReducer;
|
||||
const userToken = store.getState().loginReducer.token;
|
||||
if (!hasLogin) {
|
||||
return false;
|
||||
}
|
||||
const res = await asyncRequest<
|
||||
StandardHTTPResponse<{ permissions?: RepoPermissions }>
|
||||
>({
|
||||
const { hasLogin, githubToken } = userInfo();
|
||||
if (!hasLogin) return false;
|
||||
const res = await requestGitHub<{ permissions: GitHubRepoPermissions }>({
|
||||
url: `https://api.github.com/repos/${owner}/${repoName}`,
|
||||
headers: {
|
||||
Authorization: `token ${userToken}`,
|
||||
},
|
||||
customRequestResponse: false,
|
||||
});
|
||||
|
||||
if (res === undefined) return false;
|
||||
return !!res.data?.permissions?.maintain;
|
||||
if (!res || !res.permissions) return false;
|
||||
return (
|
||||
!!res.permissions.maintain ||
|
||||
!!res.permissions.admin ||
|
||||
!!res.permissions.push
|
||||
);
|
||||
};
|
||||
|
||||
export const getIssueByRepoInfo = async (
|
||||
|
@ -74,18 +77,6 @@ export const getIssueByRepoInfo = async (
|
|||
issueId?: string | number
|
||||
) => {
|
||||
// url such as https://api.github.com/repos/pallets/flask/issues/4333
|
||||
|
||||
const url = `https://api.github.com/repos/${owner}/${repoName}/issues/${issueId}`;
|
||||
const { hasLogin } = store.getState().loginReducer;
|
||||
const userToken = store.getState().loginReducer.token;
|
||||
const headers: any | undefined =
|
||||
hasLogin && userToken ? { Authorization: `token ${userToken}` } : undefined;
|
||||
|
||||
return await asyncRequest<StandardHTTPResponse<Partial<GitHubIssueResponse>>>(
|
||||
{
|
||||
url,
|
||||
headers,
|
||||
customRequestResponse: false,
|
||||
}
|
||||
);
|
||||
return await requestGitHub<Partial<GitHubIssueResponse>>({ url });
|
||||
};
|
||||
|
|
|
@ -1,74 +1,69 @@
|
|||
import axios from 'axios';
|
||||
import { KeyMap } from '../module/data/dataModel';
|
||||
import axios, { AxiosError } from 'axios';
|
||||
|
||||
type HTTPMethods = 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT';
|
||||
type ErrorFunc = null | ((error: Error) => void);
|
||||
type ErrorFunc = null | ((error: Error | AxiosError) => any);
|
||||
type AnyObject = { [key: string]: any };
|
||||
|
||||
type RequestParams = {
|
||||
export type RequestParams = {
|
||||
/** request method */
|
||||
method?: HTTPMethods;
|
||||
url?: string;
|
||||
params?: KeyMap;
|
||||
headers?: KeyMap;
|
||||
/** request data */
|
||||
baseURL?: string;
|
||||
/** request url */
|
||||
url: string;
|
||||
/** request params */
|
||||
params?: AnyObject;
|
||||
/** request headers */
|
||||
headers?: AnyObject;
|
||||
/** request payload */
|
||||
data?: AnyObject;
|
||||
/** error handler */
|
||||
onError?: ErrorFunc;
|
||||
customRequestResponse?: boolean;
|
||||
data?: KeyMap;
|
||||
};
|
||||
|
||||
export const URL_KEY = 'baseURL';
|
||||
|
||||
export const getBaseURL = () => {
|
||||
if (process.env.REACT_APP_ENV === 'production') {
|
||||
return process.env.REACT_APP_BASE_URL;
|
||||
if (import.meta.env.REACT_APP_ENV === 'production') {
|
||||
return import.meta.env.REACT_APP_BASE_URL;
|
||||
}
|
||||
const url = localStorage.getItem(URL_KEY);
|
||||
if (url && url.length) {
|
||||
return url;
|
||||
}
|
||||
const baseURL = process.env.REACT_APP_BASE_URL || '';
|
||||
const baseURL = import.meta.env.REACT_APP_BASE_URL || '';
|
||||
localStorage.setItem(URL_KEY, baseURL);
|
||||
return baseURL;
|
||||
};
|
||||
|
||||
/** request wrapper */
|
||||
export const asyncRequest: <T>(
|
||||
params: RequestParams
|
||||
) => Promise<T | undefined> = async (params: RequestParams) => {
|
||||
try {
|
||||
let method: HTTPMethods = 'GET';
|
||||
if (params?.method) {
|
||||
method = params.method;
|
||||
}
|
||||
if (params.customRequestResponse === undefined) {
|
||||
params.customRequestResponse = true;
|
||||
}
|
||||
|
||||
const method = params.method || 'GET';
|
||||
const baseURL = params.baseURL || getBaseURL();
|
||||
const res = await axios({
|
||||
method,
|
||||
baseURL,
|
||||
url: params.url,
|
||||
baseURL: params.baseURL,
|
||||
params: params.params,
|
||||
headers: params.headers,
|
||||
data: params.data,
|
||||
});
|
||||
|
||||
if (params.customRequestResponse) {
|
||||
if (res?.status === 200) {
|
||||
if (res.data.code === 200) {
|
||||
return res.data.result;
|
||||
}
|
||||
if (typeof params.onError === 'function') {
|
||||
return params.onError(res.data);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
throw new Error('server response failed');
|
||||
if (200 <= res.status && res.status < 300 && res.data) {
|
||||
return res.data;
|
||||
} else {
|
||||
return res;
|
||||
// use callback function to handle error
|
||||
const msg = res.data || res.statusText;
|
||||
throw new Error(msg);
|
||||
}
|
||||
} catch (error) {
|
||||
if (typeof params.onError === 'function' && error instanceof Error) {
|
||||
} catch (error: any | AxiosError) {
|
||||
// log
|
||||
console.error('%s %s: %s', params.url, error.name, error.message);
|
||||
if (typeof params.onError === 'function') {
|
||||
params.onError(error);
|
||||
}
|
||||
return error;
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ import { DescriptionPage } from './pages/descriptionPage';
|
|||
import { GFIHeader } from './pages/GFIHeader';
|
||||
import { Repositories } from './pages/repositories/repositories';
|
||||
|
||||
import { persistor, store } from './module/storage/configureStorage';
|
||||
import { persistor, store } from './storage/configureStorage';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { MainPage } from './pages/main/mainPage';
|
||||
import { LoginRedirect } from './pages/login/GFILoginComponents';
|
||||
|
@ -28,9 +28,7 @@ import { GFICopyright } from './pages/GFIComponents';
|
|||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<HelmetProvider>
|
||||
<Helmet>
|
||||
<title> GFI Bot </title>
|
||||
</Helmet>
|
||||
<Helmet></Helmet>
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<WindowContextProvider>
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
export type GFIResponse<T> = {
|
||||
code?: number;
|
||||
result: T;
|
||||
};
|
||||
|
||||
export type GFIFailure = {
|
||||
detail: string | ValidationError[];
|
||||
};
|
||||
|
||||
/** FastAPI Validation Error */
|
||||
export type ValidationError = {
|
||||
/** Location */
|
||||
loc: (Partial<string> & Partial<number>)[];
|
||||
/** Message */
|
||||
msg: string;
|
||||
/** Error Type */
|
||||
type: string;
|
||||
};
|
||||
|
||||
/** Repo Info */
|
||||
export interface RepoBrief {
|
||||
name: string;
|
||||
owner: string;
|
||||
description?: string;
|
||||
language?: string;
|
||||
topics: string[];
|
||||
}
|
||||
|
||||
type MonthlyCount = {
|
||||
/** ISO datestring */
|
||||
month: string;
|
||||
/** Number of * in the month */
|
||||
count: number;
|
||||
};
|
||||
|
||||
/** Repo Info (with monthly stats) */
|
||||
export type RepoDetail = RepoBrief & {
|
||||
monthly_stars: MonthlyCount[];
|
||||
monthly_commits: MonthlyCount[];
|
||||
monthly_issues: MonthlyCount[];
|
||||
monthly_pulls: MonthlyCount[];
|
||||
};
|
||||
|
||||
/** supported sort */
|
||||
export type RepoSort =
|
||||
| 'popularity'
|
||||
| 'gfis'
|
||||
| 'median_issue_resolve_time'
|
||||
| 'newcomer_friendly';
|
||||
|
||||
export type UserQueryHistory = {
|
||||
/** number of queries in total */
|
||||
nums: number;
|
||||
/** pending queries */
|
||||
queries: RepoBrief[];
|
||||
/** finished queries */
|
||||
finished_queries: RepoBrief[];
|
||||
};
|
||||
|
||||
export type GFIInfo = {
|
||||
name: string;
|
||||
owner: string;
|
||||
probability: number;
|
||||
number: number;
|
||||
/** ISO datestring */
|
||||
last_updated: string;
|
||||
title?: string;
|
||||
state?: 'closed' | 'open' | 'resolved';
|
||||
};
|
||||
|
||||
export type GFITrainingSummary = {
|
||||
name: string;
|
||||
owner: string;
|
||||
issues_train: number;
|
||||
issues_test: number;
|
||||
n_resolved_issues: number;
|
||||
n_newcomer_resolved: number;
|
||||
last_updated: string;
|
||||
/** performance metrics are not available during training */
|
||||
accuracy?: number;
|
||||
auc?: number;
|
||||
};
|
||||
|
||||
export type RepoGFIConfig = {
|
||||
newcomer_threshold: number;
|
||||
gfi_threshold: number;
|
||||
need_comment: boolean;
|
||||
issue_tag: string;
|
||||
};
|
||||
|
||||
export type RepoUpdateConfig = {
|
||||
task_id: string | null;
|
||||
interval: number;
|
||||
begin_time: string;
|
||||
};
|
||||
|
||||
export type RepoConfig = {
|
||||
update_config: RepoUpdateConfig;
|
||||
repo_config: RepoGFIConfig;
|
||||
};
|
||||
|
||||
export type SearchedRepo = {
|
||||
name: string;
|
||||
owner: string;
|
||||
created_at: string;
|
||||
increment: number;
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
type AnyObject = { [key: string]: any };
|
||||
|
||||
export type GitHubHTTPResponse<T extends AnyObject> = {
|
||||
[key: string]: any;
|
||||
status: number;
|
||||
data?: T;
|
||||
};
|
||||
|
||||
export type GitHubIssueResponse = {
|
||||
number: number;
|
||||
title: string;
|
||||
state: string;
|
||||
active_lock_reason: string;
|
||||
body: string;
|
||||
html_url: string;
|
||||
};
|
||||
|
||||
export type GitHubRepoPermissions = {
|
||||
admin: boolean;
|
||||
maintain: boolean;
|
||||
push: boolean;
|
||||
triage: boolean;
|
||||
pull: boolean;
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
import { RepoBrief } from './api';
|
||||
|
||||
export const MockedRepoInfo: RepoBrief = {
|
||||
name: 'scikit-learn',
|
||||
owner: 'scikit-learn',
|
||||
language: 'Python',
|
||||
description: 'scikit-learn: machine learning in Python',
|
||||
topics: ['python', 'data-science', 'machine-learning'],
|
||||
};
|
|
@ -1,86 +0,0 @@
|
|||
export type KeyMap = { [key: string]: any };
|
||||
|
||||
export type StandardHTTPResponse<T extends KeyMap> = {
|
||||
[key: string]: any;
|
||||
status: number;
|
||||
data?: KeyMap & T;
|
||||
};
|
||||
|
||||
export interface GFIRepoInfo {
|
||||
name: string;
|
||||
owner: string;
|
||||
description?: string;
|
||||
url?: string;
|
||||
topics?: string[];
|
||||
}
|
||||
|
||||
export type GetRepoDetailedInfo = {
|
||||
name?: string;
|
||||
description?: string;
|
||||
language?: string[];
|
||||
monthly_stars?: string[];
|
||||
monthly_commits?: string[];
|
||||
monthly_issues?: string[];
|
||||
monthly_pulls?: string[];
|
||||
};
|
||||
|
||||
export type RepoPermissions = {
|
||||
admin: boolean;
|
||||
maintain: boolean;
|
||||
push: boolean;
|
||||
triage: boolean;
|
||||
pull: boolean;
|
||||
};
|
||||
|
||||
export type GFIUserQueryHistoryItem = {
|
||||
pending: boolean;
|
||||
repo: GFIRepoInfo;
|
||||
};
|
||||
|
||||
export type GFIInfo = {
|
||||
name: string;
|
||||
owner: string;
|
||||
probability: number;
|
||||
number: number;
|
||||
last_updated: string;
|
||||
};
|
||||
|
||||
export type GitHubIssueResponse = {
|
||||
number: number;
|
||||
title: string;
|
||||
state: string;
|
||||
active_lock_reason: string;
|
||||
body: string;
|
||||
html_url: string;
|
||||
};
|
||||
|
||||
export type GFITrainingSummary = {
|
||||
name: string;
|
||||
owner: string;
|
||||
issues_train: number;
|
||||
issues_test: number;
|
||||
n_resolved_issues: number;
|
||||
n_newcomer_resolved: number;
|
||||
accuracy: number;
|
||||
auc: number;
|
||||
last_updated: string;
|
||||
};
|
||||
|
||||
export type GFIRepoConfig = {
|
||||
newcomer_threshold: number;
|
||||
gfi_threshold: number;
|
||||
need_comment: boolean;
|
||||
issue_tag: string;
|
||||
};
|
||||
|
||||
export type GFIRepoUpdateConfig = {
|
||||
interval: number;
|
||||
begin_time: string;
|
||||
};
|
||||
|
||||
export type GFIUserSearch = {
|
||||
name: string;
|
||||
owner: string;
|
||||
created_at: string;
|
||||
increment: number;
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
GFIIssueMonitor,
|
||||
GFIRepoDisplayView,
|
||||
} from '../../pages/main/GFIRepoDisplayView';
|
||||
import { GFIRepoInfo } from './dataModel';
|
||||
import { Repositories } from '../../pages/repositories/repositories';
|
||||
|
||||
export const MockedRepoInfo: GFIRepoInfo = {
|
||||
name: 'scikit-learn',
|
||||
owner: 'scikit-learn',
|
||||
description: 'scikit-learn: machine learning in Python',
|
||||
topics: ['python', 'data-science', 'machine-learning'],
|
||||
};
|
|
@ -17,7 +17,7 @@ import { Variant as AlarmPanelVariants } from 'react-bootstrap/types';
|
|||
|
||||
export function GFICopyright() {
|
||||
const copyright =
|
||||
'Copyright © 2021 OSS Lab, Peking University. All rights reserved.';
|
||||
'Copyright © 2022 OSS Lab, Peking University. All rights reserved.';
|
||||
|
||||
return (
|
||||
<Container
|
||||
|
@ -470,6 +470,8 @@ export const GFIOverlay = forwardRef<HTMLDivElement, GFIOverlay>(
|
|||
}
|
||||
);
|
||||
|
||||
GFIOverlay.displayName = 'GFIOverlay';
|
||||
|
||||
export function GFISimplePagination(props: {
|
||||
nums: number;
|
||||
onClick: (idx: number) => void;
|
||||
|
|
|
@ -29,7 +29,7 @@ import { gitHubLogin } from '../api/githubApi';
|
|||
import {
|
||||
createAccountNavStateAction,
|
||||
createLogoutAction,
|
||||
} from '../module/storage/reducers';
|
||||
} from '../storage/reducers';
|
||||
import '../style/gfiStyle.css';
|
||||
|
||||
import navLogo from '../assets/favicon-thumbnail.png';
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Container, ToastContainer, Toast, Button } from 'react-bootstrap';
|
|||
import '../../style/gfiStyle.css';
|
||||
import { UserOutlined } from '@ant-design/icons';
|
||||
import { defaultFontFamily } from '../../utils';
|
||||
import { createLoginAction } from '../../module/storage/reducers';
|
||||
import { createLoginAction } from '../../storage/reducers';
|
||||
|
||||
export function LoginRedirect(props: any) {
|
||||
const dispatch = useDispatch();
|
||||
|
|
|
@ -25,15 +25,16 @@ import {
|
|||
} from '../GFIComponents';
|
||||
import {
|
||||
GFIInfo,
|
||||
GFIRepoInfo,
|
||||
GetRepoDetailedInfo,
|
||||
RepoBrief,
|
||||
RepoDetail,
|
||||
GFITrainingSummary,
|
||||
} from '../../module/data/dataModel';
|
||||
} from '../../model/api';
|
||||
import { getIssueByRepoInfo } from '../../api/githubApi';
|
||||
import { GFIRootReducers } from '../../module/storage/configureStorage';
|
||||
import { createPopoverAction } from '../../module/storage/reducers';
|
||||
import { GFIRootReducers } from '../../storage/configureStorage';
|
||||
import { createPopoverAction } from '../../storage/reducers';
|
||||
import {
|
||||
getGFIByRepoName,
|
||||
getGFINum,
|
||||
getRepoDetailedInfo,
|
||||
getTrainingSummary,
|
||||
} from '../../api/api';
|
||||
|
@ -48,7 +49,7 @@ export interface RepoShouldDisplayPopoverState {
|
|||
}
|
||||
|
||||
export interface GFIRepoBasicProp {
|
||||
repoInfo: GFIRepoInfo;
|
||||
repoInfo: RepoBrief;
|
||||
}
|
||||
|
||||
export interface GFIRepoDisplayView extends GFIRepoBasicProp {
|
||||
|
@ -112,6 +113,7 @@ export const GFIRepoDisplayView = forwardRef(
|
|||
<div
|
||||
className="flex-col"
|
||||
style={i === selectedTag ? {} : { display: 'none' }}
|
||||
key={i}
|
||||
>
|
||||
<RepoDisplayOverlayIDProvider id={overlayID}>
|
||||
{node}
|
||||
|
@ -126,7 +128,11 @@ export const GFIRepoDisplayView = forwardRef(
|
|||
function Title() {
|
||||
const ProjectTags = () => {
|
||||
return repoInfo.topics?.map((item, i) => {
|
||||
return <div className="repo-display-info-repo-tag">{item}</div>;
|
||||
return (
|
||||
<div className="repo-display-info-repo-tag" key={i}>
|
||||
{item}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -150,6 +156,7 @@ export const GFIRepoDisplayView = forwardRef(
|
|||
<PanelTag
|
||||
name={item}
|
||||
id={i}
|
||||
key={i}
|
||||
onClick={(id) => {
|
||||
if (id !== selectedTag) {
|
||||
setSelectedTag(id);
|
||||
|
@ -207,6 +214,8 @@ export const GFIRepoDisplayView = forwardRef(
|
|||
}
|
||||
);
|
||||
|
||||
GFIRepoDisplayView.displayName = 'GFIRepoDisplayView';
|
||||
|
||||
function PanelTag(props: {
|
||||
name: string;
|
||||
id: number;
|
||||
|
@ -254,48 +263,54 @@ export const GFIIssueMonitor = forwardRef((props: GFIIssueMonitor, ref) => {
|
|||
const [currentPageIdx, setCurrentPageIdx] = useState(1);
|
||||
const [pageInput, setPageInput] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [gfiNum, setGfiNum] = useState(0);
|
||||
|
||||
const onUpdate = async () => {
|
||||
if (!displayIssueList || !gfiNum) {
|
||||
// not loading
|
||||
const num = await getGFINum(repoInfo.name, repoInfo.owner);
|
||||
setGfiNum(num);
|
||||
setShouldDisplayPagination(num > maxPageItems);
|
||||
}
|
||||
const pageLowerBound = (currentPageIdx - 1) * maxPageItems;
|
||||
const res = await getGFIByRepoName(
|
||||
repoInfo.name,
|
||||
repoInfo.owner,
|
||||
pageLowerBound,
|
||||
maxPageItems
|
||||
);
|
||||
if (Array.isArray(res) && res.length) {
|
||||
setDisplayIssueList(res);
|
||||
} else {
|
||||
setDisplayIssueList(undefined);
|
||||
setIsLoading(false);
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!displayIssueList) {
|
||||
getGFIByRepoName(repoInfo.name, repoInfo.owner).then((res) => {
|
||||
if (Array.isArray(res) && res.length) {
|
||||
setDisplayIssueList(res);
|
||||
setShouldDisplayPagination(res.length > maxPageItems);
|
||||
} else {
|
||||
setDisplayIssueList(undefined);
|
||||
setIsLoading(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [repoInfo]);
|
||||
onUpdate();
|
||||
}, [currentPageIdx]);
|
||||
|
||||
const render = () => {
|
||||
const pageLowerBound = (currentPageIdx - 1) * maxPageItems;
|
||||
const pageUpperBound = currentPageIdx * maxPageItems;
|
||||
// const pageLowerBound = (currentPageIdx - 1) * maxPageItems;
|
||||
// const pageUpperBound = currentPageIdx * maxPageItems;
|
||||
const randomId = Math.random() * 1000;
|
||||
return displayIssueList?.map((issue, i) => {
|
||||
if (pageLowerBound <= i && i < pageUpperBound) {
|
||||
return (
|
||||
<GFIIssueListItem
|
||||
repoInfo={repoInfo}
|
||||
issue={issue}
|
||||
key={`gfi-issue-${repoInfo.name}-${issue}-${i}-${randomId}`}
|
||||
useTips={!(i % maxPageItems)}
|
||||
trainingSummary={trainingSummary}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
});
|
||||
return displayIssueList?.map((issue, i) => (
|
||||
<GFIIssueListItem
|
||||
repoInfo={repoInfo}
|
||||
issue={issue}
|
||||
key={`gfi-issue-${repoInfo.name}-${issue}-${i}-${randomId}`}
|
||||
useTips={!(i % maxPageItems)}
|
||||
trainingSummary={trainingSummary}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const onPageBtnClicked = useCallback(() => {
|
||||
if (pageInput && checkIsNumber(pageInput) && displayIssueList) {
|
||||
const page = parseInt(pageInput, 10);
|
||||
if (
|
||||
page > 0 &&
|
||||
page <= Math.ceil(displayIssueList.length / maxPageItems)
|
||||
) {
|
||||
if (page > 0 && page <= Math.ceil(gfiNum / maxPageItems)) {
|
||||
setCurrentPageIdx(parseInt(pageInput, 10));
|
||||
}
|
||||
}
|
||||
|
@ -323,7 +338,7 @@ export const GFIIssueMonitor = forwardRef((props: GFIIssueMonitor, ref) => {
|
|||
>
|
||||
<GFIPagination
|
||||
maxPagingCount={3}
|
||||
pageNums={Math.ceil(displayIssueList.length / maxPageItems)}
|
||||
pageNums={Math.ceil(gfiNum / maxPageItems)}
|
||||
pageIdx={currentPageIdx}
|
||||
toPage={(page) => setCurrentPageIdx(page)}
|
||||
needInputArea
|
||||
|
@ -339,6 +354,8 @@ export const GFIIssueMonitor = forwardRef((props: GFIIssueMonitor, ref) => {
|
|||
);
|
||||
});
|
||||
|
||||
GFIIssueMonitor.displayName = 'GFIIssueMonitor';
|
||||
|
||||
export interface GFIIssueListItem extends GFIRepoBasicProp {
|
||||
issue: GFIInfo;
|
||||
useTips: boolean;
|
||||
|
@ -349,10 +366,10 @@ type IssueState = 'closed' | 'open' | 'resolved';
|
|||
interface IssueDisplayData {
|
||||
issueId: number;
|
||||
title: string;
|
||||
body: string;
|
||||
body?: string;
|
||||
state: IssueState;
|
||||
url: string;
|
||||
gfi: GFIInfo;
|
||||
gfi?: GFIInfo;
|
||||
}
|
||||
|
||||
function GFIIssueListItem(props: GFIIssueListItem) {
|
||||
|
@ -361,31 +378,42 @@ function GFIIssueListItem(props: GFIIssueListItem) {
|
|||
const { repoInfo, issue, useTips, trainingSummary } = props;
|
||||
const [displayData, setDisplayData] = useState<IssueDisplayData>();
|
||||
|
||||
useEffect(() => {
|
||||
getIssueByRepoInfo(repoInfo.name, repoInfo.owner, issue.number).then(
|
||||
(res) => {
|
||||
if (res && res.status === 200) {
|
||||
if (res.data && !checkHasUndefinedProperty(res.data)) {
|
||||
let issueState = 'open';
|
||||
if (res.data.state === 'closed') {
|
||||
issueState = 'closed';
|
||||
}
|
||||
if (res.data.active_lock_reason === 'resolved') {
|
||||
issueState = 'resolved';
|
||||
}
|
||||
setDisplayData({
|
||||
issueId: res.data.number as number,
|
||||
title: res.data.title as string,
|
||||
body: res.data.body as string,
|
||||
state: issueState as IssueState,
|
||||
url: res.data.html_url as string,
|
||||
gfi: issue,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
}
|
||||
}
|
||||
const updateIssue = async () => {
|
||||
const res = await getIssueByRepoInfo(
|
||||
repoInfo.name,
|
||||
repoInfo.owner,
|
||||
issue.number
|
||||
);
|
||||
if (res) {
|
||||
let issueState: IssueState = 'open';
|
||||
if (res.state === 'closed') {
|
||||
issueState = 'closed';
|
||||
}
|
||||
if (res.active_lock_reason === 'resolved') {
|
||||
issueState = 'resolved';
|
||||
}
|
||||
setDisplayData({
|
||||
issueId: res.number,
|
||||
title: res.title,
|
||||
body: res.body,
|
||||
state: issueState,
|
||||
url: res.html_url,
|
||||
gfi: issue,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// use backend data first
|
||||
setDisplayData({
|
||||
issueId: issue.number,
|
||||
title: issue.title,
|
||||
state: issue.state,
|
||||
url: `https://github.com/${repoInfo.owner}/${repoInfo.name}/issues/${issue.number}`,
|
||||
gfi: issue,
|
||||
});
|
||||
// update from github later
|
||||
updateIssue();
|
||||
}, []);
|
||||
|
||||
const issueBtn = () => {
|
||||
|
@ -450,7 +478,9 @@ function GFIIssueListItem(props: GFIIssueListItem) {
|
|||
useTips ? 'tool-tips' : ''
|
||||
}`}
|
||||
>
|
||||
{`${(issue.probability * 100).toFixed(2)}%`}
|
||||
{`${(issue.probability * 100).toFixed(
|
||||
issue.probability > 0.99995 ? 1 : 2
|
||||
)}%`}
|
||||
{useTips && (
|
||||
<div className="tool-tips-text-top flex-row align-center justify-content-center">
|
||||
GFI Probability
|
||||
|
@ -501,6 +531,7 @@ function IssueOverlayItem(props: IssueOverlayItem) {
|
|||
]
|
||||
: [];
|
||||
|
||||
/* eslint-disable react/no-children-prop */
|
||||
return (
|
||||
<div
|
||||
className="flex-col repo-overlay-item"
|
||||
|
@ -521,14 +552,16 @@ function IssueOverlayItem(props: IssueOverlayItem) {
|
|||
className="flex-row align-center justify-content-start flex-wrap"
|
||||
style={{ marginBottom: '0.2rem' }}
|
||||
>
|
||||
{repoInfo.topics?.map((item) => (
|
||||
<div className="repo-display-info-repo-tag">{item}</div>
|
||||
{repoInfo.topics?.map((item, i) => (
|
||||
<div className="repo-display-info-repo-tag" key={i}>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{simpleTrainDataProps && (
|
||||
<div className="flex-row issue-demo-data-container-overlay">
|
||||
{simpleTrainDataProps.map((prop) => (
|
||||
<SimpleTrainInfoTag title={prop.title} data={prop.data} />
|
||||
{simpleTrainDataProps.map((prop, i) => (
|
||||
<SimpleTrainInfoTag title={prop.title} data={prop.data} key={i} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
@ -562,6 +595,7 @@ function IssueOverlayItem(props: IssueOverlayItem) {
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
/* eslint-enable react/no-children-prop */
|
||||
}
|
||||
|
||||
export interface GFIRepoStaticsDemonstrator extends GFIRepoBasicProp {
|
||||
|
@ -573,7 +607,7 @@ export const GFIRepoStaticsDemonstrator = forwardRef(
|
|||
(props: GFIRepoStaticsDemonstrator, ref) => {
|
||||
const { repoInfo, trainingSummary, paging } = props;
|
||||
const usePaging = !(paging === false && paging !== undefined);
|
||||
const [displayInfo, setDisplayInfo] = useState<GetRepoDetailedInfo>();
|
||||
const [displayInfo, setDisplayInfo] = useState<RepoDetail>();
|
||||
const simpleTrainDataProps: SimpleTrainInfoTagProp[] | [] = trainingSummary
|
||||
? [
|
||||
{
|
||||
|
@ -623,7 +657,7 @@ export const GFIRepoStaticsDemonstrator = forwardRef(
|
|||
|
||||
useEffect(() => {
|
||||
getRepoDetailedInfo(repoInfo.name, repoInfo.owner).then((res) => {
|
||||
const result = res as GetRepoDetailedInfo;
|
||||
const result = res as RepoDetail;
|
||||
setDisplayInfo(result);
|
||||
});
|
||||
}, []);
|
||||
|
@ -657,6 +691,7 @@ export const GFIRepoStaticsDemonstrator = forwardRef(
|
|||
? {}
|
||||
: { display: 'none' }
|
||||
}
|
||||
key={idx}
|
||||
>
|
||||
<RepoGraphContainer
|
||||
title={dataTitle[idx]}
|
||||
|
@ -673,8 +708,8 @@ export const GFIRepoStaticsDemonstrator = forwardRef(
|
|||
<>
|
||||
{simpleTrainDataProps && (
|
||||
<div className="flex-row issue-demo-data-container">
|
||||
{simpleTrainDataProps.map((prop) => (
|
||||
<SimpleTrainInfoTag title={prop.title} data={prop.data} />
|
||||
{simpleTrainDataProps.map((prop, i) => (
|
||||
<SimpleTrainInfoTag title={prop.title} data={prop.data} key={i} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
@ -699,6 +734,8 @@ export const GFIRepoStaticsDemonstrator = forwardRef(
|
|||
}
|
||||
);
|
||||
|
||||
GFIRepoStaticsDemonstrator.displayName = 'GFIRepoStaticsDemonstrator';
|
||||
|
||||
interface SimpleTrainInfoTagProp {
|
||||
title: string;
|
||||
data: number;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { ForwardedRef, forwardRef, useEffect, useState } from 'react';
|
||||
import { GFITrainingSummary } from '../../module/data/dataModel';
|
||||
import type { GFITrainingSummary } from '../../model/api';
|
||||
import { getGFINum, getTrainingSummary } from '../../api/api';
|
||||
|
||||
import '../../style/gfiStyle.css';
|
||||
|
@ -254,6 +254,8 @@ export const GFITrainingSummaryDisplayView = forwardRef(
|
|||
}
|
||||
);
|
||||
|
||||
GFITrainingSummaryDisplayView.displayName = 'GFITrainingSummaryDisplayView';
|
||||
|
||||
function NumInfoDisplayer(props: {
|
||||
width: number;
|
||||
height: number;
|
||||
|
@ -264,10 +266,9 @@ function NumInfoDisplayer(props: {
|
|||
}) {
|
||||
const { width, height, gradient, gradientId, num, title } = props;
|
||||
|
||||
// @ts-nocheck
|
||||
return (
|
||||
<>
|
||||
{/*
|
||||
// @ts-ignore */}
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
|
@ -390,6 +391,7 @@ function ActivityDisplayer(props: {
|
|||
const marginX = (width - graphWidth * 0.8) / 2.0;
|
||||
const marginY = (height - graphHeight) / 2.0 - 4;
|
||||
|
||||
/* eslint-disable react/jsx-key */
|
||||
return (
|
||||
<svg width={width} height={height}>
|
||||
<g className="flex-row align-center justify-content-center">
|
||||
|
@ -445,4 +447,5 @@ function ActivityDisplayer(props: {
|
|||
</g>
|
||||
</svg>
|
||||
);
|
||||
/* eslint-enable react/jsx-key */
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ import './mainPage.css';
|
|||
import '../../style/gfiStyle.css';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getLanguageTags } from '../../api/api';
|
||||
import { GFIRootReducers } from '../../module/storage/configureStorage';
|
||||
import { MainPageLangTagSelectedState } from '../../module/storage/reducers';
|
||||
import { GFIRootReducers } from '../../storage/configureStorage';
|
||||
import { MainPageLangTagSelectedState } from '../../storage/reducers';
|
||||
|
||||
export type GFIRepoSearchingFilterType =
|
||||
| 'None'
|
||||
|
@ -53,6 +53,7 @@ export const GFIMainPageHeader = forwardRef((props: GFIMainPageHeader, ref) => {
|
|||
style={{
|
||||
fontSize: 'small',
|
||||
}}
|
||||
key={title}
|
||||
>
|
||||
{title}
|
||||
</Dropdown.Item>
|
||||
|
@ -202,3 +203,5 @@ export const GFIMainPageHeader = forwardRef((props: GFIMainPageHeader, ref) => {
|
|||
</Container>
|
||||
);
|
||||
});
|
||||
|
||||
GFIMainPageHeader.displayName = 'GFIMainPageHeader';
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
checkIsNumber,
|
||||
defaultFontFamily,
|
||||
checkIsGitRepoURL,
|
||||
convertFilter,
|
||||
} from '../../utils';
|
||||
|
||||
import { GFINotiToast } from '../login/GFILoginComponents';
|
||||
|
@ -21,6 +22,7 @@ import {
|
|||
getPagedRepoDetailedInfo,
|
||||
getTrainingSummary,
|
||||
getRepoInfo,
|
||||
getPagedRepoBrief,
|
||||
} from '../../api/api';
|
||||
import { checkGithubLogin } from '../../api/githubApi';
|
||||
|
||||
|
@ -30,7 +32,7 @@ import {
|
|||
createMainPageLangTagSelectedAction,
|
||||
createPopoverAction,
|
||||
MainPageLangTagSelectedState,
|
||||
} from '../../module/storage/reducers';
|
||||
} from '../../storage/reducers';
|
||||
import { GFI_REPO_FILTER_NONE, GFIMainPageHeader } from './mainHeader';
|
||||
|
||||
import {
|
||||
|
@ -38,10 +40,9 @@ import {
|
|||
GFIRepoDisplayView,
|
||||
GFIRepoStaticsDemonstrator,
|
||||
} from './GFIRepoDisplayView';
|
||||
import { GFIRepoInfo, GFITrainingSummary } from '../../module/data/dataModel';
|
||||
import { GFIRootReducers } from '../../module/storage/configureStorage';
|
||||
import { RepoBrief, GFITrainingSummary, RepoSort } from '../../model/api';
|
||||
import { GFIRootReducers } from '../../storage/configureStorage';
|
||||
import { GFITrainingSummaryDisplayView } from './GFITrainingSummaryDisplayView';
|
||||
import { GFIAlphaWarning } from './GFIBanners';
|
||||
|
||||
export function MainPage() {
|
||||
const dispatch = useDispatch();
|
||||
|
@ -69,15 +70,16 @@ export function MainPage() {
|
|||
return state.loginReducer?.avatar;
|
||||
});
|
||||
|
||||
const emptyRepoInfo: GFIRepoInfo = {
|
||||
const emptyRepoInfo: RepoBrief = {
|
||||
name: '',
|
||||
owner: '',
|
||||
description: '',
|
||||
url: '',
|
||||
language: '',
|
||||
topics: [],
|
||||
};
|
||||
|
||||
const [displayRepoInfo, setDisplayRepoInfo] = useState<
|
||||
GFIRepoInfo[] | undefined
|
||||
RepoBrief[] | undefined
|
||||
>([emptyRepoInfo]);
|
||||
const [alarmConfig, setAlarmConfig] = useState({ show: false, msg: '' });
|
||||
|
||||
|
@ -145,7 +147,7 @@ export function MainPage() {
|
|||
|
||||
useEffect(() => {
|
||||
if (selectedTag || selectedFilter) {
|
||||
fetchRepoInfoList(1, selectedTag, selectedFilter);
|
||||
fetchRepoInfoList(1, selectedTag, convertFilter(selectedFilter));
|
||||
setPageIdx(1);
|
||||
dispatch(
|
||||
createMainPageLangTagSelectedAction({
|
||||
|
@ -157,7 +159,7 @@ export function MainPage() {
|
|||
|
||||
useEffect(() => {
|
||||
if (pageIdx) {
|
||||
fetchRepoInfoList(pageIdx, selectedTag, selectedFilter);
|
||||
fetchRepoInfoList(pageIdx, selectedTag, convertFilter(selectedFilter));
|
||||
}
|
||||
}, [pageIdx]);
|
||||
|
||||
|
@ -192,7 +194,7 @@ export function MainPage() {
|
|||
const fetchRepoInfoList = (
|
||||
pageNum: number,
|
||||
tag?: string,
|
||||
filter?: string
|
||||
filter?: RepoSort
|
||||
) => {
|
||||
const beginIdx = (pageNum - 1) * repoCapacity;
|
||||
dispatch(createGlobalProgressBarAction({ hidden: false }));
|
||||
|
@ -201,27 +203,24 @@ export function MainPage() {
|
|||
setTotalRepos(res);
|
||||
}
|
||||
});
|
||||
getPagedRepoDetailedInfo(beginIdx, repoCapacity, tag, filter).then(
|
||||
(repoList) => {
|
||||
if (repoList && Array.isArray(repoList)) {
|
||||
const repoInfoList = repoList.map((repo, i) => {
|
||||
if ('name' in repo && 'owner' in repo) {
|
||||
return {
|
||||
name: repo.name,
|
||||
owner: repo.owner,
|
||||
description:
|
||||
'description' in repo ? repo.description : undefined,
|
||||
topics: 'topics' in repo ? repo.topics : undefined,
|
||||
url: '',
|
||||
};
|
||||
}
|
||||
return emptyRepoInfo;
|
||||
});
|
||||
setDisplayRepoInfo(repoInfoList);
|
||||
}
|
||||
dispatch(createGlobalProgressBarAction({ hidden: true }));
|
||||
getPagedRepoBrief(beginIdx, repoCapacity, tag, filter).then((repoList) => {
|
||||
if (repoList && Array.isArray(repoList)) {
|
||||
const repoInfoList = repoList.map((repo) => {
|
||||
if ('name' in repo && 'owner' in repo) {
|
||||
return {
|
||||
name: repo.name,
|
||||
owner: repo.owner,
|
||||
language: repo.language ? repo.language : undefined,
|
||||
description: repo.description ? repo.description : undefined,
|
||||
topics: 'topics' in repo ? repo.topics : undefined,
|
||||
};
|
||||
}
|
||||
return emptyRepoInfo;
|
||||
});
|
||||
setDisplayRepoInfo(repoInfoList);
|
||||
}
|
||||
);
|
||||
dispatch(createGlobalProgressBarAction({ hidden: true }));
|
||||
});
|
||||
};
|
||||
|
||||
const onPageBtnClicked = () => {
|
||||
|
@ -268,10 +267,15 @@ export function MainPage() {
|
|||
repoInfo={item}
|
||||
tags={['GFI', 'Repo Data']}
|
||||
panels={[
|
||||
<GFIIssueMonitor repoInfo={item} trainingSummary={summary} />,
|
||||
<GFIIssueMonitor
|
||||
repoInfo={item}
|
||||
trainingSummary={summary}
|
||||
key={1}
|
||||
/>,
|
||||
<GFIRepoStaticsDemonstrator
|
||||
repoInfo={item}
|
||||
trainingSummary={summary}
|
||||
key={2}
|
||||
/>,
|
||||
]}
|
||||
style={{
|
||||
|
@ -403,13 +407,13 @@ export function MainPage() {
|
|||
</Row>
|
||||
<Row>
|
||||
<GFINotiToast
|
||||
show={showBannerMsg}
|
||||
userName={userName || 'visitor'}
|
||||
userAvatarUrl={userAvatarUrl}
|
||||
onClose={() => {
|
||||
setShowBannerMsg(false);
|
||||
}}
|
||||
context="GFI-Bot is under active development and not ready for production yet."
|
||||
show={showBannerMsg}
|
||||
userName={userName || 'visitor'}
|
||||
userAvatarUrl={userAvatarUrl}
|
||||
onClose={() => {
|
||||
setShowBannerMsg(false);
|
||||
}}
|
||||
context="GFI-Bot is under active development and not ready for production yet."
|
||||
/>
|
||||
<GFINotiToast
|
||||
show={showLoginMsg}
|
||||
|
@ -492,7 +496,7 @@ const GFIDadaKanban = forwardRef((props: GFIDadaKanban, ref) => {
|
|||
<button
|
||||
className={`gfi-rounded ${selected}`}
|
||||
key={`lang-tag ${index}`}
|
||||
onClick={(e) => {
|
||||
onClick={() => {
|
||||
if (index !== selectedIdx) {
|
||||
setSelectedIdx(index);
|
||||
onTagClicked(val);
|
||||
|
@ -527,3 +531,5 @@ const GFIDadaKanban = forwardRef((props: GFIDadaKanban, ref) => {
|
|||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
GFIDadaKanban.displayName = 'GFIDadaKanban';
|
||||
|
|
|
@ -21,18 +21,15 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||
import {
|
||||
createAccountNavStateAction,
|
||||
createGlobalProgressBarAction,
|
||||
} from '../../module/storage/reducers';
|
||||
import { GFIRootReducers } from '../../module/storage/configureStorage';
|
||||
import { checkIsGitRepoURL } from '../../utils';
|
||||
} from '../../storage/reducers';
|
||||
import { GFIRootReducers } from '../../storage/configureStorage';
|
||||
import { checkIsGitRepoURL, convertFilter } from '../../utils';
|
||||
|
||||
import importTips from '../../assets/git-add-demo.png';
|
||||
import { checkHasRepoPermissions } from '../../api/githubApi';
|
||||
import { GFIAlarm, GFIAlarmPanelVariants, GFIOverlay } from '../GFIComponents';
|
||||
import { addRepoToGFIBot, getAddRepoHistory } from '../../api/api';
|
||||
import {
|
||||
GFIRepoInfo,
|
||||
GFIUserQueryHistoryItem,
|
||||
} from '../../module/data/dataModel';
|
||||
import type { RepoBrief } from '../../model/api';
|
||||
import {
|
||||
GFIIssueMonitor,
|
||||
GFIRepoDisplayView,
|
||||
|
@ -44,6 +41,10 @@ import { SearchHistory } from './SearchHistory';
|
|||
import { RepoSetting } from './RepoSetting';
|
||||
|
||||
export interface GFIPortal {}
|
||||
type GFIUserQueryHistoryItem = {
|
||||
pending: boolean;
|
||||
repo: RepoBrief;
|
||||
};
|
||||
|
||||
type SubPanelIDs = 'Add Project' | 'Search History' | 'My Account';
|
||||
const SubPanelTitles: SubPanelIDs[] & string[] = [
|
||||
|
@ -161,6 +162,7 @@ function AccountSideBar(props: AccountSideBar) {
|
|||
}
|
||||
}}
|
||||
variant={selectedList[i] ? 'primary' : 'light'}
|
||||
key={i}
|
||||
>
|
||||
{title}
|
||||
</ListGroup.Item>
|
||||
|
@ -215,7 +217,7 @@ function AddProjectComponent() {
|
|||
const [addedRepos, setAddedRepos] = useState<GFIUserQueryHistoryItem[]>();
|
||||
const [addedRepoIncrement, setAddedRepoIncrement] = useState(false);
|
||||
const fetchAddedRepos = (onComplete?: () => void) => {
|
||||
getAddRepoHistory(filterSelected).then((res) => {
|
||||
getAddRepoHistory(convertFilter(filterSelected)).then((res) => {
|
||||
const finishedQueries: GFIUserQueryHistoryItem[] | undefined =
|
||||
res?.finished_queries?.map((info) => ({
|
||||
pending: false,
|
||||
|
@ -352,7 +354,7 @@ function AddProjectComponent() {
|
|||
|
||||
const repoInfoPanelRef = useRef<HTMLDivElement>(null);
|
||||
const [addedRepoDisplayPanelConfig, setAddedRepoDisplayPanelConfig] =
|
||||
useState<GFIRepoInfo>();
|
||||
useState<RepoBrief>();
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
|
||||
type FilterType = GFIRepoSearchingFilterType;
|
||||
|
@ -365,20 +367,21 @@ function AddProjectComponent() {
|
|||
'Newcomer Friendliness',
|
||||
];
|
||||
|
||||
const onRepoHistoryClicked = (repoInfo: GFIRepoInfo) => {
|
||||
const onRepoHistoryClicked = (repoInfo: RepoBrief) => {
|
||||
setAddedRepoDisplayPanelConfig(repoInfo);
|
||||
setShowPopover(true);
|
||||
};
|
||||
|
||||
const renderRepoHistory = () => {
|
||||
if (addedRepos && addedRepos.length) {
|
||||
return addedRepos.map((item) => {
|
||||
return addedRepos.map((item, i) => {
|
||||
return (
|
||||
<RepoHistoryTag
|
||||
pending={item.pending}
|
||||
repoInfo={item.repo}
|
||||
available
|
||||
onClick={item.pending ? () => {} : onRepoHistoryClicked}
|
||||
key={i}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -389,6 +392,9 @@ function AddProjectComponent() {
|
|||
repoInfo={{
|
||||
name: 'None',
|
||||
owner: 'Try to add your projects!',
|
||||
description: '',
|
||||
language: '',
|
||||
topics: [],
|
||||
}}
|
||||
available={false}
|
||||
/>
|
||||
|
@ -429,7 +435,7 @@ function AddProjectComponent() {
|
|||
<div className="project-add-comp-tips">
|
||||
<p>
|
||||
{' '}
|
||||
<strong>Notice: </strong> We'll register the repository to our
|
||||
<strong>Notice: </strong> We'll register the repository to our
|
||||
database and use it for data training and predictions.{' '}
|
||||
</p>
|
||||
<p>
|
||||
|
@ -469,7 +475,6 @@ function AddProjectComponent() {
|
|||
</div>
|
||||
<Overlay
|
||||
show={showOverlay}
|
||||
// @ts-ignore
|
||||
target={overlayTarget}
|
||||
container={overlayContainer}
|
||||
placement="bottom-start"
|
||||
|
@ -519,6 +524,7 @@ function AddProjectComponent() {
|
|||
onFilterSelected(item);
|
||||
}}
|
||||
style={{ fontSize: 'small' }}
|
||||
key={item}
|
||||
>
|
||||
{item as string}
|
||||
</Dropdown.Item>
|
||||
|
@ -569,11 +575,15 @@ function AddProjectComponent() {
|
|||
repoInfo={addedRepoDisplayPanelConfig}
|
||||
tags={['Settings', 'GFI', 'Repo Data']}
|
||||
panels={[
|
||||
<RepoSetting repoInfo={addedRepoDisplayPanelConfig} />,
|
||||
<GFIIssueMonitor repoInfo={addedRepoDisplayPanelConfig} />,
|
||||
<RepoSetting repoInfo={addedRepoDisplayPanelConfig} key={1} />,
|
||||
<GFIIssueMonitor
|
||||
repoInfo={addedRepoDisplayPanelConfig}
|
||||
key={2}
|
||||
/>,
|
||||
<GFIRepoStaticsDemonstrator
|
||||
repoInfo={addedRepoDisplayPanelConfig}
|
||||
paging={false}
|
||||
key={3}
|
||||
/>,
|
||||
]}
|
||||
style={{
|
||||
|
@ -605,9 +615,9 @@ function AddProjectComponent() {
|
|||
|
||||
function RepoHistoryTag(props: {
|
||||
pending: boolean;
|
||||
repoInfo: GFIRepoInfo;
|
||||
repoInfo: RepoBrief;
|
||||
available: boolean;
|
||||
onClick?: (repoInfo: GFIRepoInfo) => void;
|
||||
onClick?: (repoInfo: RepoBrief) => void;
|
||||
}) {
|
||||
const { pending, repoInfo, available, onClick } = props;
|
||||
const isPending = available
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
updateRepoConfig,
|
||||
updateRepoInfo,
|
||||
} from '../../api/api';
|
||||
import { GFIRepoConfig } from '../../module/data/dataModel';
|
||||
import type { RepoGFIConfig } from '../../model/api';
|
||||
import { checkIsNumber } from '../../utils';
|
||||
|
||||
export type RepoSettingPops = GFIRepoBasicProp;
|
||||
|
@ -24,7 +24,7 @@ export function RepoSetting(props: RepoSettingPops) {
|
|||
const [showComment, setShowComment] = useState(false);
|
||||
const [newcomerThresholdSelected, setNewcomerThresholdSelected] = useState(1);
|
||||
const [showDeleteAlarm, setShowDeleteAlarm] = useState(false);
|
||||
const [currentRepoConfig, setCurrentRepoConfig] = useState<GFIRepoConfig>();
|
||||
const [currentRepoConfig, setCurrentRepoConfig] = useState<RepoGFIConfig>();
|
||||
const [showConfigAlarmBanner, setShowConfigAlarmBanner] = useState(false);
|
||||
const [configAlarmBanner, setConfigAlarmBanner] = useState<{
|
||||
variant: GFIAlarmPanelVariants;
|
||||
|
@ -69,7 +69,7 @@ export function RepoSetting(props: RepoSettingPops) {
|
|||
parseFloat(gfiThreshold) < 1 &&
|
||||
gfiTag
|
||||
) {
|
||||
const repoConfig: GFIRepoConfig = {
|
||||
const repoConfig: RepoGFIConfig = {
|
||||
newcomer_threshold: newcomerThresholdSelected,
|
||||
issue_tag: gfiTag,
|
||||
gfi_threshold: parseFloat(gfiThreshold),
|
||||
|
@ -171,6 +171,7 @@ export function RepoSetting(props: RepoSettingPops) {
|
|||
{[0, 1, 2, 3, 4].map((i, idx) => (
|
||||
<option
|
||||
selected={idx + 1 === newcomerThresholdSelected}
|
||||
key={idx}
|
||||
>
|
||||
{i + 1}
|
||||
</option>
|
||||
|
@ -246,8 +247,8 @@ export function RepoSetting(props: RepoSettingPops) {
|
|||
{showDeleteAlarm && (
|
||||
<GFIAlarm className="no-btn gfi-repo-setting-alarm">
|
||||
<div>
|
||||
{' '}
|
||||
Warning: You're going to delete your repository in GFI-Bot{' '}
|
||||
Warning: You're going to delete your repository in
|
||||
GFI-Bot
|
||||
</div>
|
||||
<div className="flex-row gfi-repo-setting-alarm-btns">
|
||||
<Button
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from 'react';
|
|||
|
||||
import { ListGroup } from 'react-bootstrap';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { GFIRepoInfo, GFIUserSearch } from '../../module/data/dataModel';
|
||||
import type { RepoBrief, SearchedRepo } from '../../model/api';
|
||||
import { deleteUserSearch, getRepoInfo, getUserSearches } from '../../api/api';
|
||||
|
||||
import '../../style/gfiStyle.css';
|
||||
|
@ -15,12 +15,12 @@ import { GFIOverlay } from '../GFIComponents';
|
|||
import { useIsMobile } from '../app/windowContext';
|
||||
|
||||
export function SearchHistory() {
|
||||
const [searchHistory, setSearchHistory] = useState<GFIUserSearch[]>();
|
||||
const [searchHistory, setSearchHistory] = useState<SearchedRepo[]>();
|
||||
const [showPopover, setShowPopover] = useState(false);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const setSearchRes = (res: GFIUserSearch[]) => {
|
||||
const setSearchRes = (res: SearchedRepo[]) => {
|
||||
const res_reversed = res.reverse();
|
||||
setSearchHistory(res_reversed);
|
||||
setSelectedList(res_reversed.map((_, idx) => !idx));
|
||||
|
@ -35,7 +35,7 @@ export function SearchHistory() {
|
|||
}, []);
|
||||
|
||||
const [selectedList, setSelectedList] = useState<boolean[]>();
|
||||
const [repoDisplay, setRepoDisplay] = useState<GFIRepoInfo>();
|
||||
const [repoDisplay, setRepoDisplay] = useState<RepoBrief>();
|
||||
|
||||
const onItemClicked = (name: string, owner: string, idx: number) => {
|
||||
if (repoDisplay?.name !== name || repoDisplay?.owner !== owner) {
|
||||
|
@ -70,7 +70,7 @@ export function SearchHistory() {
|
|||
numTag += ' gfi-list-last';
|
||||
}
|
||||
return (
|
||||
<div className="flex-row align-center">
|
||||
<div className="flex-row align-center" key={idx}>
|
||||
<ListGroup.Item
|
||||
className={`gfi-search-history-item-wrapper ${numTag}`}
|
||||
id={`gfi-search-history-item-${item.owner}-${item.name}-${idx}`}
|
||||
|
@ -125,10 +125,11 @@ export function SearchHistory() {
|
|||
repoInfo={repoDisplay}
|
||||
tags={['GFI', 'Repo Data']}
|
||||
panels={[
|
||||
<GFIIssueMonitor repoInfo={repoDisplay} paging={14} />,
|
||||
<GFIIssueMonitor repoInfo={repoDisplay} paging={14} key={1} />,
|
||||
<GFIRepoStaticsDemonstrator
|
||||
repoInfo={repoDisplay}
|
||||
paging={false}
|
||||
key={2}
|
||||
/>,
|
||||
]}
|
||||
style={{
|
||||
|
|
|
@ -20,12 +20,20 @@ export const RepoGraphContainer = (props: RepoGraphContainerProps) => {
|
|||
});
|
||||
};
|
||||
|
||||
// desciption for ISO date format
|
||||
// https://www.w3schools.com/js/js_date_methods.asp
|
||||
const dateDescriptor = (date: string) =>
|
||||
new Date(date).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
});
|
||||
|
||||
const issueDataParser = (info: any[] | undefined) => {
|
||||
if (typeof info !== 'undefined') {
|
||||
return info.map((tempInfo, i) => {
|
||||
return {
|
||||
count: tempInfo.count,
|
||||
month: tempInfo.month.slice(8, 16),
|
||||
month: dateDescriptor(tempInfo.month),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,8 +2,6 @@ import React, { useEffect, useState } from 'react';
|
|||
import { Alert, Badge, Col, Container, ListGroup, Row } from 'react-bootstrap';
|
||||
import '../../style/gfiStyle.css';
|
||||
|
||||
// @ts-ignore
|
||||
import Fade from '@stahl.luke/react-reveal/Fade';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { checkIsNumber } from '../../utils';
|
||||
import { GFIAlarm, GFIPagination } from '../GFIComponents';
|
||||
|
@ -14,7 +12,7 @@ import { getRepoNum, getPagedRepoDetailedInfo } from '../../api/api';
|
|||
import {
|
||||
createAccountNavStateAction,
|
||||
createGlobalProgressBarAction,
|
||||
} from '../../module/storage/reducers';
|
||||
} from '../../storage/reducers';
|
||||
|
||||
export function Repositories() {
|
||||
const repoListCapacity = 5;
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
mainPageLangTagSelectedStateReducer,
|
||||
showMainPagePopoverReducer,
|
||||
} from './reducers';
|
||||
import { RepoShouldDisplayPopoverState } from '../../pages/main/GFIRepoDisplayView';
|
||||
import { RepoShouldDisplayPopoverState } from '../pages/main/GFIRepoDisplayView';
|
||||
|
||||
const persistConfig = {
|
||||
key: 'root',
|
|
@ -0,0 +1,8 @@
|
|||
import { store } from './configureStorage';
|
||||
|
||||
export const userInfo = () => ({
|
||||
hasLogin: store.getState().loginReducer.hasLogin,
|
||||
name: store.getState().loginReducer.name,
|
||||
githubLogin: store.getState().loginReducer.loginName,
|
||||
githubToken: store.getState().loginReducer.token,
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import { Reducer } from 'redux';
|
||||
import { RepoShouldDisplayPopoverState } from '../../pages/main/GFIRepoDisplayView';
|
||||
import { RepoShouldDisplayPopoverState } from '../pages/main/GFIRepoDisplayView';
|
||||
|
||||
export type LoginState = {
|
||||
hasLogin: boolean;
|
|
@ -206,6 +206,7 @@ code {
|
|||
}
|
||||
|
||||
.sign-in {
|
||||
width: max-content;
|
||||
width: -webkit-max-content;
|
||||
width: -moz-fit-content;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { GFIRepoSearchingFilterType } from './pages/main/mainHeader';
|
||||
import { RepoSort } from './model/api';
|
||||
|
||||
export const checkIsNumber = (val: string | number | undefined) => {
|
||||
const reg = /^\d+.?\d*/;
|
||||
|
@ -46,27 +47,24 @@ const repoFilters = [
|
|||
'gfis',
|
||||
];
|
||||
|
||||
export const convertFilter = (filter: string | undefined) => {
|
||||
let filterConverted: string | undefined;
|
||||
if (filter) {
|
||||
switch (filter as GFIRepoSearchingFilterType) {
|
||||
case 'Popularity':
|
||||
filterConverted = repoFilters[0];
|
||||
break;
|
||||
case 'Median Issue Resolve Time':
|
||||
filterConverted = repoFilters[1];
|
||||
break;
|
||||
case 'Newcomer Friendliness':
|
||||
filterConverted = repoFilters[2];
|
||||
break;
|
||||
case 'GFIs':
|
||||
filterConverted = repoFilters[3];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return filterConverted;
|
||||
const filterNames = {
|
||||
popularity: 'Popularity',
|
||||
median_issue_resolve_time: 'Median Issue Resolve Time',
|
||||
newcomer_friendly: 'Newcomer Friendliness',
|
||||
gfis: 'GFIs',
|
||||
};
|
||||
|
||||
const nameToFilter = Object.fromEntries(
|
||||
Object.entries(filterNames).map(([k, v]) => [v, k])
|
||||
);
|
||||
|
||||
/** convert semantic filter names -> backend args */
|
||||
export const convertFilter = (s: string): RepoSort | undefined => {
|
||||
if (s in Object.keys(filterNames)) {
|
||||
return s as RepoSort;
|
||||
} else if (s in Object.keys(nameToFilter)) {
|
||||
return nameToFilter[s] as RepoSort;
|
||||
} else return undefined;
|
||||
};
|
||||
|
||||
export const checkIsValidUrl = (url: string) => {
|
||||
|
|
|
@ -11,7 +11,7 @@ export default defineConfig(({command, mode}) => {
|
|||
env = { ...env, ...processEnv };
|
||||
console.log(mode, env);
|
||||
|
||||
let clientPort = 3000;
|
||||
let clientPort = undefined;
|
||||
if ("GFIBOT_HTTPS_PORT" in process.env) {
|
||||
clientPort = parseInt(process.env.GFIBOT_HTTPS_PORT);
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ export default defineConfig(({command, mode}) => {
|
|||
},
|
||||
server: {
|
||||
hmr: {
|
||||
clientPort: 8443,
|
||||
clientPort: clientPort,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,8 +24,8 @@ class RepoQuery(BaseModel):
|
|||
class RepoBrief(BaseModel):
|
||||
name: str
|
||||
owner: str
|
||||
description: str
|
||||
language: str
|
||||
description: Optional[str]
|
||||
language: Optional[str]
|
||||
topics: List[str]
|
||||
|
||||
|
||||
|
@ -91,6 +91,8 @@ class GFIBrief(BaseModel):
|
|||
threshold: float
|
||||
probability: float
|
||||
last_updated: datetime
|
||||
state: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
|
||||
|
||||
class TrainingResult(BaseModel):
|
||||
|
|
|
@ -42,15 +42,26 @@ def get_gfi_brief(
|
|||
Prediction.objects(
|
||||
Q(name=repo) & Q(owner=owner) & Q(probability__gte=threshold)
|
||||
)
|
||||
.only(*GFIBrief.__fields__)
|
||||
.order_by("-probability")
|
||||
.only("name", "owner", "number", "threshold", "probability", "last_updated")
|
||||
.order_by(
|
||||
"-probability", "-number"
|
||||
) # probability may be the same -> repeated issue
|
||||
)
|
||||
|
||||
if start is not None and length is not None:
|
||||
gfi_list = gfi_list.skip(start).limit(length)
|
||||
|
||||
if gfi_list:
|
||||
return GFIResponse(result=[GFIBrief(**gfi.to_mongo()) for gfi in gfi_list])
|
||||
res_list: List[GFIBrief] = []
|
||||
for gfi in gfi_list:
|
||||
issue: RepoIssue = RepoIssue.objects(
|
||||
Q(name=repo) & Q(owner=owner) & Q(number=gfi.number)
|
||||
).first()
|
||||
res_dict = (
|
||||
{**gfi.to_mongo(), **issue.to_mongo()} if issue else gfi.to_mongo()
|
||||
)
|
||||
res_list.append(GFIBrief(**res_dict))
|
||||
return GFIResponse(result=res_list)
|
||||
raise HTTPException(status_code=404, detail="Good first issue not found")
|
||||
|
||||
|
||||
|
|
|
@ -132,6 +132,44 @@ def get_paged_repo_detail(
|
|||
# return GFIResponse(result=repos_brief)
|
||||
|
||||
|
||||
@api.get("/info/paged", response_model=GFIResponse[List[RepoBrief]])
|
||||
def get_paged_repo_brief(
|
||||
start: int,
|
||||
length: int,
|
||||
lang: Optional[str] = None,
|
||||
filter: Optional[RepoSort] = None,
|
||||
):
|
||||
"""
|
||||
Get brief info of repository (paged)
|
||||
"""
|
||||
q = Repo.objects()
|
||||
|
||||
if lang:
|
||||
q = q.filter(language=lang)
|
||||
|
||||
if filter:
|
||||
if filter == RepoSort.GFIS:
|
||||
q = q.order_by("-n_gfis")
|
||||
elif filter == RepoSort.ISSUE_CLOSE_TIME:
|
||||
q = q.order_by("issue_close_time")
|
||||
elif filter == RepoSort.NEWCOMER_RESOLVE_RATE:
|
||||
q = q.order_by("-r_newcomer_resolve")
|
||||
elif filter == RepoSort.STARS:
|
||||
q = q.order_by("-n_stars")
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Invalid filter: expect one in {}".format(RepoSort.__members__),
|
||||
)
|
||||
else:
|
||||
q = q.order_by("name")
|
||||
|
||||
repos_list = [
|
||||
r.to_mongo() for r in q.skip(start).limit(length).only(*RepoBrief.__fields__)
|
||||
]
|
||||
return GFIResponse(result=repos_list)
|
||||
|
||||
|
||||
@api.get("/info/search", response_model=GFIResponse[List[RepoDetail]])
|
||||
def search_repo_detail(
|
||||
user: Optional[str] = None, repo: Optional[str] = None, url: Optional[str] = None
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"eslint-plugin-react-svg": "^0.0.4"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue