Merge pull request #28 from 12f23eddde/fix-date

Frontend fixes
This commit is contained in:
Hao He 2022-08-15 16:25:17 +08:00 committed by GitHub
commit b284ea8537
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 3486 additions and 494 deletions

17
.github/workflows/publish-cf-pages.yml vendored Normal file
View File

@ -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 }}

View File

@ -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

View File

@ -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"
}
}

View File

@ -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" />
<!--

View File

@ -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"
}
}

View File

@ -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,
});
};

View File

@ -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 });
};

View File

@ -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;
}
};
};

View File

@ -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>

107
frontend/src/model/api.d.ts vendored Normal file
View File

@ -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;
};

24
frontend/src/model/github.d.ts vendored Normal file
View File

@ -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;
};

View File

@ -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'],
};

View File

@ -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;
};

View File

@ -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'],
};

View File

@ -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;

View File

@ -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';

View File

@ -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();

View File

@ -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;

View File

@ -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 */
}

View File

@ -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';

View File

@ -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';

View File

@ -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&apos;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

View File

@ -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{' '}
&nbsp;Warning: You&apos;re going to delete your repository in
GFI-Bot&nbsp;
</div>
<div className="flex-row gfi-repo-setting-alarm-btns">
<Button

View File

@ -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={{

View File

@ -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),
};
});
}

View File

@ -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;

View File

@ -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',

View File

@ -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,
});

View File

@ -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;

View File

@ -206,6 +206,7 @@ code {
}
.sign-in {
width: max-content;
width: -webkit-max-content;
width: -moz-fit-content;
}

View File

@ -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) => {

View File

@ -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,
},
}
}

View File

@ -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):

View File

@ -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")

View File

@ -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

2825
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"devDependencies": {
"eslint-plugin-react-svg": "^0.0.4"
}
}