@stackoverflow/backstage-plugin-stack-overflow-teams 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/config.d.ts +71 -0
- package/dist/api/StackOverflowAPI.esm.js +125 -0
- package/dist/api/StackOverflowAPI.esm.js.map +1 -0
- package/dist/components/StackOverflow/StackOverflowMe.esm.js +110 -0
- package/dist/components/StackOverflow/StackOverflowMe.esm.js.map +1 -0
- package/dist/components/StackOverflow/StackOverflowPostQuestionModal.esm.js +406 -0
- package/dist/components/StackOverflow/StackOverflowPostQuestionModal.esm.js.map +1 -0
- package/dist/components/StackOverflow/StackOverflowPosts.esm.js +402 -0
- package/dist/components/StackOverflow/StackOverflowPosts.esm.js.map +1 -0
- package/dist/components/StackOverflow/StackOverflowSearchResultListItem.esm.js +147 -0
- package/dist/components/StackOverflow/StackOverflowSearchResultListItem.esm.js.map +1 -0
- package/dist/components/StackOverflow/StackOverflowTags.esm.js +117 -0
- package/dist/components/StackOverflow/StackOverflowTags.esm.js.map +1 -0
- package/dist/components/StackOverflow/StackOverflowUsers.esm.js +193 -0
- package/dist/components/StackOverflow/StackOverflowUsers.esm.js.map +1 -0
- package/dist/components/StackOverflow/TiptapEditor.esm.js +308 -0
- package/dist/components/StackOverflow/TiptapEditor.esm.js.map +1 -0
- package/dist/components/StackOverflow/hooks/useStackOverflowData.esm.js +128 -0
- package/dist/components/StackOverflow/hooks/useStackOverflowData.esm.js.map +1 -0
- package/dist/components/StackOverflow/hooks/useStackOverflowSearch.esm.js +53 -0
- package/dist/components/StackOverflow/hooks/useStackOverflowSearch.esm.js.map +1 -0
- package/dist/components/StackOverflow/hooks/useStackOverflowStyles.esm.js +39 -0
- package/dist/components/StackOverflow/hooks/useStackOverflowStyles.esm.js.map +1 -0
- package/dist/components/StackOverflowAuth/StackAuthCallback.esm.js +47 -0
- package/dist/components/StackOverflowAuth/StackAuthCallback.esm.js.map +1 -0
- package/dist/components/StackOverflowAuth/StackAuthFailed.esm.js +35 -0
- package/dist/components/StackOverflowAuth/StackAuthFailed.esm.js.map +1 -0
- package/dist/components/StackOverflowAuth/StackAuthLoading.esm.js +20 -0
- package/dist/components/StackOverflowAuth/StackAuthLoading.esm.js.map +1 -0
- package/dist/components/StackOverflowAuth/StackAuthStart.esm.js +225 -0
- package/dist/components/StackOverflowAuth/StackAuthStart.esm.js.map +1 -0
- package/dist/components/StackOverflowAuth/StackAuthSuccess.esm.js +37 -0
- package/dist/components/StackOverflowAuth/StackAuthSuccess.esm.js.map +1 -0
- package/dist/icons/LogoutIcon.esm.js +24 -0
- package/dist/icons/LogoutIcon.esm.js.map +1 -0
- package/dist/icons/StackOverflowIcon.esm.js +25 -0
- package/dist/icons/StackOverflowIcon.esm.js.map +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.esm.js +18 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/package.json.esm.js +98 -0
- package/dist/package.json.esm.js.map +1 -0
- package/dist/pages/StackOverflowHub.esm.js +82 -0
- package/dist/pages/StackOverflowHub.esm.js.map +1 -0
- package/dist/pages/StackOverflowTeamsPage.esm.js +42 -0
- package/dist/pages/StackOverflowTeamsPage.esm.js.map +1 -0
- package/dist/pages/index.esm.js +3 -0
- package/dist/pages/index.esm.js.map +1 -0
- package/dist/plugin.esm.js +27 -0
- package/dist/plugin.esm.js.map +1 -0
- package/dist/routes.esm.js +8 -0
- package/dist/routes.esm.js.map +1 -0
- package/dist/utils/decodeHtml.esm.js +8 -0
- package/dist/utils/decodeHtml.esm.js.map +1 -0
- package/dist/utils/getTimeAgo.esm.js +30 -0
- package/dist/utils/getTimeAgo.esm.js.map +1 -0
- package/package.json +88 -0
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Stack Overflow Teams Frontend Plugin
|
|
2
|
+
|
|
3
|
+
This package is the frontend counterpart of the `stack-overflow-teams` plugin for Backstage.
|
|
4
|
+
|
|
5
|
+
## Areas of Responsibility
|
|
6
|
+
|
|
7
|
+
It provides the UI and interacts with the [backend service](https://github.com/EstoesMoises/backstage-stackoverflow/tree/main/plugins/stack-overflow-teams-backend) to fetch data from your Stack Overflow Enterprise instance.
|
|
8
|
+
|
|
9
|
+
### Backend Dependency
|
|
10
|
+
|
|
11
|
+
To fully utilize this plugin, you must also install and configure the corresponding **backend package** (`backstage-plugin-stack-overflow-teams-backend`) in your Backstage backend. The frontend plugin relies on the backend for API communication and authentication handling.
|
|
12
|
+
|
|
13
|
+
## More details
|
|
14
|
+
|
|
15
|
+
### Enhanced `<StackOverflowSearchResultListItem />`
|
|
16
|
+
|
|
17
|
+
This component is a modified version of the [community plugin.](https://github.com/backstage/community-plugins/tree/main/workspaces/stack-overflow/plugins/stack-overflow/src/search/StackOverflowSearchResultListItem)
|
|
18
|
+
|
|
19
|
+
It adds a more Stack Overflow-like interface, including additional information such as the questions' score, user role, user reputation, and timestamp.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
### Individual Frontend Components
|
|
24
|
+
|
|
25
|
+
- **`<StackOverflowMe />`**
|
|
26
|
+
|
|
27
|
+
Displays information about the authenticated user.
|
|
28
|
+
|
|
29
|
+
- **`<StackOverflowPostQuestionModal />`**
|
|
30
|
+
|
|
31
|
+
Provides a form for users to create a new Stack Overflow question. Once submitted, an API request is executed to create the question.
|
|
32
|
+
|
|
33
|
+
This form listens to the `'openAskQuestionModal'` event. You can utilize this anywhere in your Backstage UI. To invoke the form, add the component to your UI along with a button that dispatches the event. Example:
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
<SidebarItem
|
|
37
|
+
icon={StackOverflowIcon}
|
|
38
|
+
onClick={() => window.dispatchEvent(new Event('openAskQuestionModal'))}
|
|
39
|
+
text="Ask a Question"
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<StackOverflowPostQuestionModal />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- **`<StackOverflowQuestion />`**
|
|
46
|
+
|
|
47
|
+
Retrieves questions from the API. Uses standard pagination, displaying only the first 30 API results.
|
|
48
|
+
|
|
49
|
+
- **`<StackOverflowTags />`**
|
|
50
|
+
|
|
51
|
+
Retrieves tags from the API. Uses standard pagination, displaying only the first 30 API results.
|
|
52
|
+
|
|
53
|
+
- **`<StackOverflowUsers />`**
|
|
54
|
+
|
|
55
|
+
Retrieves users from the API. Uses standard pagination, displaying only the first 30 API results.
|
|
56
|
+
|
|
57
|
+
- **`<StackOverflowHub />`**
|
|
58
|
+
|
|
59
|
+
Various components collectively create this informative hub.
|
|
60
|
+
|
|
61
|
+
## Authentication Components
|
|
62
|
+
|
|
63
|
+
- **`<StackAuthStart />`**
|
|
64
|
+
|
|
65
|
+
Initiates **`/auth/start`** on the backend.
|
|
66
|
+
|
|
67
|
+
- **`<StackAuthLoading />`**
|
|
68
|
+
|
|
69
|
+
Handles the loading state during authentication.
|
|
70
|
+
|
|
71
|
+
- **`<StackAuthCallback />`**
|
|
72
|
+
|
|
73
|
+
Receives the code and state from your Stack Overflow Enterprise instance as part of the OAuth process and initiates **`/callback`** in the backend.
|
|
74
|
+
|
|
75
|
+
- **`<StackAuthSuccess />`**
|
|
76
|
+
|
|
77
|
+
Displays authentication success state.
|
|
78
|
+
|
|
79
|
+
- **`<StackAuthFailed />`**
|
|
80
|
+
|
|
81
|
+
Displays authentication failure state.
|
|
82
|
+
|
|
83
|
+
### Page
|
|
84
|
+
|
|
85
|
+
- **`<StackOverflowTeamsPage />`**
|
|
86
|
+
|
|
87
|
+
This page triggers authentication components, bundles, and orchestrates everything for you so you don't have to use the authentication components separately. If authenticated, it will return the `<StackOverflowHub />`.
|
|
88
|
+
|
|
89
|
+
### API Requests
|
|
90
|
+
|
|
91
|
+
The frontend plugin creates an API Ref for Stack Overflow for Teams, which can be found under the `/api` folder. **All API requests from the frontend are directed to Backstage's backend**.
|
package/config.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2023 The Backstage Authors
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export interface Config {
|
|
18
|
+
/**
|
|
19
|
+
* Configuration options for the stack overflow for teams frontend plugin.
|
|
20
|
+
*
|
|
21
|
+
* This configuration is shared with the backstage-plugin-stack-overflow-teams-backend and backstage-stack-overflow-teams-collator
|
|
22
|
+
*/
|
|
23
|
+
stackoverflow?: {
|
|
24
|
+
/**
|
|
25
|
+
* The base url of the Stack Overflow API used for the plugin
|
|
26
|
+
*/
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The API Access Token to authenticate to Stack Overflow API Version 3
|
|
31
|
+
* @visibility secret
|
|
32
|
+
*/
|
|
33
|
+
apiAccessToken: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The name of the team for a Stack Overflow for Teams account
|
|
37
|
+
*/
|
|
38
|
+
teamName?: string;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Client Id for the OAuth Application, required to use the Stack Overflow for Teams Hub and write actions.
|
|
42
|
+
*/
|
|
43
|
+
clientId: number;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* RedirectUri for the OAuth Application, required to use the Stack Overflow for Teams Hub and write actions.
|
|
47
|
+
*
|
|
48
|
+
* This should be your Backstage application domain ending in the plugin's <StackOverflowTeamsPage /> route
|
|
49
|
+
*/
|
|
50
|
+
redirectUri: string;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Type representing the request parameters.
|
|
54
|
+
*/
|
|
55
|
+
requestParams?: {
|
|
56
|
+
[key: string]: string | string[] | number;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Type representing the schedule options.
|
|
60
|
+
*/
|
|
61
|
+
schedule?: {
|
|
62
|
+
frequency?: TimeUnit;
|
|
63
|
+
timeout?: TimeUnit;
|
|
64
|
+
initialDelay?: TimeUnit;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type TimeUnit = { minutes?: number; hours?: number; seconds?: number }
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createApiRef } from '@backstage/core-plugin-api';
|
|
2
|
+
|
|
3
|
+
const stackoverflowteamsApiRef = createApiRef({
|
|
4
|
+
id: "plugin.stackoverflowteams.api"
|
|
5
|
+
});
|
|
6
|
+
const createStackOverflowApi = (discoveryApi, fetchApi) => {
|
|
7
|
+
const getBaseUrl = async () => discoveryApi.getBaseUrl("stack-overflow-teams");
|
|
8
|
+
const requestAPI = async (endpoint, method = "GET", body, params) => {
|
|
9
|
+
const baseUrl = await getBaseUrl();
|
|
10
|
+
const queryString = params ? `?${params.join("&")}` : "";
|
|
11
|
+
const url = `${baseUrl}/${endpoint}${queryString}`;
|
|
12
|
+
const response = await fetchApi.fetch(url, {
|
|
13
|
+
method,
|
|
14
|
+
credentials: "include",
|
|
15
|
+
headers: { "Content-Type": "application/json" },
|
|
16
|
+
body: body ? JSON.stringify(body) : void 0
|
|
17
|
+
});
|
|
18
|
+
if (!response.ok) {
|
|
19
|
+
const errorResponse = await response.json();
|
|
20
|
+
throw new Error(errorResponse.error || `Request failed: ${response.statusText}`);
|
|
21
|
+
}
|
|
22
|
+
return response.json();
|
|
23
|
+
};
|
|
24
|
+
const buildQuestionParams = (filter) => {
|
|
25
|
+
const params = [];
|
|
26
|
+
if (filter?.sort) {
|
|
27
|
+
params.push(`sort=${filter.sort}`);
|
|
28
|
+
}
|
|
29
|
+
if (filter?.order) {
|
|
30
|
+
params.push(`order=${filter.order}`);
|
|
31
|
+
}
|
|
32
|
+
if (filter?.isAnswered !== void 0) {
|
|
33
|
+
params.push(`isAnswered=${filter.isAnswered}`);
|
|
34
|
+
}
|
|
35
|
+
if (filter?.page !== void 0) {
|
|
36
|
+
params.push(`page=${filter.page}`);
|
|
37
|
+
}
|
|
38
|
+
if (filter?.pageSize !== void 0) {
|
|
39
|
+
params.push(`pageSize=${filter.pageSize}`);
|
|
40
|
+
}
|
|
41
|
+
return params;
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
search: (query, page) => {
|
|
45
|
+
const body = { query };
|
|
46
|
+
if (page !== void 0) {
|
|
47
|
+
body.page = page;
|
|
48
|
+
}
|
|
49
|
+
return requestAPI("search", "POST", body);
|
|
50
|
+
},
|
|
51
|
+
getQuestions: (filter) => {
|
|
52
|
+
const params = buildQuestionParams(filter);
|
|
53
|
+
return requestAPI("questions", "GET", void 0, params.length > 0 ? params : void 0);
|
|
54
|
+
},
|
|
55
|
+
getTags: (search) => {
|
|
56
|
+
const params = search ? [`search=${encodeURIComponent(search)}`] : void 0;
|
|
57
|
+
return requestAPI("tags", "GET", void 0, params);
|
|
58
|
+
},
|
|
59
|
+
getUsers: () => requestAPI("users"),
|
|
60
|
+
getMe: () => requestAPI("me"),
|
|
61
|
+
getBaseUrl: async () => {
|
|
62
|
+
const response = await requestAPI("baseurl");
|
|
63
|
+
return response.SOInstance;
|
|
64
|
+
},
|
|
65
|
+
getTeamName: async () => {
|
|
66
|
+
const response = await requestAPI("baseurl");
|
|
67
|
+
return response.teamName;
|
|
68
|
+
},
|
|
69
|
+
postQuestion: (title, body, tags) => requestAPI("questions", "POST", { title, body, tags }),
|
|
70
|
+
startAuth: async () => {
|
|
71
|
+
const data = await requestAPI("auth/start");
|
|
72
|
+
return data.authUrl;
|
|
73
|
+
},
|
|
74
|
+
completeAuth: async (code, state) => {
|
|
75
|
+
await requestAPI("callback", "GET", void 0, [
|
|
76
|
+
`code=${encodeURIComponent(code)}`,
|
|
77
|
+
`state=${encodeURIComponent(state)}`
|
|
78
|
+
]);
|
|
79
|
+
},
|
|
80
|
+
getAuthStatus: async () => {
|
|
81
|
+
try {
|
|
82
|
+
await requestAPI("authStatus");
|
|
83
|
+
return true;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
logout: async () => {
|
|
89
|
+
try {
|
|
90
|
+
await requestAPI("logout", "POST");
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
submitAccessToken: async (token) => {
|
|
97
|
+
try {
|
|
98
|
+
await requestAPI("auth/token", "POST", { accessToken: token });
|
|
99
|
+
return true;
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
// Convenience methods for common filtering scenarios
|
|
105
|
+
getActiveQuestions: (page) => {
|
|
106
|
+
const params = buildQuestionParams({ sort: "activity", order: "desc", page });
|
|
107
|
+
return requestAPI("questions", "GET", void 0, params);
|
|
108
|
+
},
|
|
109
|
+
getNewestQuestions: (page) => {
|
|
110
|
+
const params = buildQuestionParams({ sort: "creation", order: "desc", page });
|
|
111
|
+
return requestAPI("questions", "GET", void 0, params);
|
|
112
|
+
},
|
|
113
|
+
getTopScoredQuestions: (page) => {
|
|
114
|
+
const params = buildQuestionParams({ sort: "score", order: "desc", page });
|
|
115
|
+
return requestAPI("questions", "GET", void 0, params);
|
|
116
|
+
},
|
|
117
|
+
getUnansweredQuestions: (page) => {
|
|
118
|
+
const params = buildQuestionParams({ isAnswered: false, sort: "creation", order: "desc", page });
|
|
119
|
+
return requestAPI("questions", "GET", void 0, params);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export { createStackOverflowApi, stackoverflowteamsApiRef };
|
|
125
|
+
//# sourceMappingURL=StackOverflowAPI.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StackOverflowAPI.esm.js","sources":["../../src/api/StackOverflowAPI.ts"],"sourcesContent":["import {\n createApiRef,\n DiscoveryApi,\n FetchApi,\n} from '@backstage/core-plugin-api';\nimport { Question, Tag, User, PaginatedResponse } from '../api';\n\nexport const stackoverflowteamsApiRef = createApiRef<StackOverflowAPI>({\n id: 'plugin.stackoverflowteams.api',\n});\n\ntype ApiResponse<T> = PaginatedResponse<T>;\n\ninterface BaseUrlResponse {\n SOInstance: string;\n teamName: string\n}\n\n// Enhanced interface for questions filter options\ninterface QuestionsFilter {\n sort?: 'activity' | 'creation' | 'score';\n order?: 'asc' | 'desc';\n isAnswered?: boolean;\n page?: number;\n pageSize?: number;\n}\n\nexport interface StackOverflowAPI {\n search(query: string, page?: number): Promise<any>;\n getQuestions(filter?: QuestionsFilter): Promise<ApiResponse<Question>>;\n getTags(search?: string): Promise<ApiResponse<Tag>>;\n getUsers(): Promise<ApiResponse<User>>;\n getMe(): Promise<User>;\n getBaseUrl(): Promise<string>;\n getTeamName(): Promise<string>;\n postQuestion(title: string, body: string, tags: string[]): Promise<Question>;\n startAuth(): Promise<string>;\n completeAuth(code: string, state: string): Promise<void>;\n getAuthStatus: () => Promise<boolean>;\n logout: () => Promise<boolean>;\n submitAccessToken: (token: string) => Promise<boolean>;\n \n // Convenience methods for common filtering scenarios\n getActiveQuestions(page?: number): Promise<ApiResponse<Question>>;\n getNewestQuestions(page?: number): Promise<ApiResponse<Question>>;\n getTopScoredQuestions(page?: number): Promise<ApiResponse<Question>>;\n getUnansweredQuestions(page?: number): Promise<ApiResponse<Question>>;\n}\n\nexport const createStackOverflowApi = (\n discoveryApi: DiscoveryApi,\n fetchApi: FetchApi,\n): StackOverflowAPI => {\n const getBaseUrl = async () => discoveryApi.getBaseUrl('stack-overflow-teams');\n\n const requestAPI = async <T>(\n endpoint: string,\n method: 'GET' | 'POST' = 'GET',\n body?: Record<string, unknown>,\n params?: string[],\n ): Promise<T> => {\n const baseUrl = await getBaseUrl();\n const queryString = params ? `?${params.join('&')}` : '';\n const url = `${baseUrl}/${endpoint}${queryString}`;\n \n const response = await fetchApi.fetch(url, {\n method,\n credentials: 'include',\n headers: { 'Content-Type': 'application/json' },\n body: body ? JSON.stringify(body) : undefined,\n });\n\n if (!response.ok) {\n const errorResponse = await response.json();\n throw new Error(errorResponse.error || `Request failed: ${response.statusText}`);\n }\n\n return response.json();\n };\n\n const buildQuestionParams = (filter?: QuestionsFilter): string[] => {\n const params: string[] = [];\n \n if (filter?.sort) {\n params.push(`sort=${filter.sort}`);\n }\n \n if (filter?.order) {\n params.push(`order=${filter.order}`);\n }\n \n if (filter?.isAnswered !== undefined) {\n params.push(`isAnswered=${filter.isAnswered}`);\n }\n \n if (filter?.page !== undefined) {\n params.push(`page=${filter.page}`);\n }\n \n if (filter?.pageSize !== undefined) {\n params.push(`pageSize=${filter.pageSize}`);\n }\n \n return params;\n };\n\n return {\n search: (query: string, page?: number) => {\n const body: { query: string; page?: number } = { query };\n if (page !== undefined) {\n body.page = page;\n }\n return requestAPI<any>('search', 'POST', body);\n },\n getQuestions: (filter?: QuestionsFilter) => {\n const params = buildQuestionParams(filter);\n return requestAPI<ApiResponse<Question>>('questions', 'GET', undefined, params.length > 0 ? params : undefined);\n },\n getTags: (search?: string) => {\n const params = search ? [`search=${encodeURIComponent(search)}`] : undefined;\n return requestAPI<ApiResponse<Tag>>('tags', 'GET', undefined, params);\n },\n getUsers: () => requestAPI<ApiResponse<User>>('users'),\n getMe: () => requestAPI<User>('me'),\n getBaseUrl: async () => {\n const response = await requestAPI<BaseUrlResponse>('baseurl');\n return response.SOInstance;\n },\n getTeamName: async () => {\n const response = await requestAPI<BaseUrlResponse>('baseurl')\n return response.teamName\n },\n postQuestion: (title: string, body: string, tags: string[]) =>\n requestAPI<Question>('questions', 'POST', { title, body, tags }),\n startAuth: async () => {\n const data = await requestAPI<{ authUrl: string }>('auth/start');\n return data.authUrl;\n },\n completeAuth: async (code: string, state: string) => {\n await requestAPI('callback', 'GET', undefined, [\n `code=${encodeURIComponent(code)}`,\n `state=${encodeURIComponent(state)}`,\n ]);\n },\n getAuthStatus: async (): Promise<boolean> => {\n try {\n await requestAPI('authStatus');\n return true;\n } catch {\n return false;\n }\n },\n logout: async (): Promise<boolean> => {\n try {\n await requestAPI('logout', 'POST');\n return true;\n } catch {\n return false;\n }\n },\n submitAccessToken: async (token: string): Promise<boolean> => {\n try {\n await requestAPI('auth/token', 'POST', { accessToken: token });\n return true;\n } catch {\n return false;\n }\n },\n\n // Convenience methods for common filtering scenarios\n getActiveQuestions: (page?: number) => {\n const params = buildQuestionParams({ sort: 'activity', order: 'desc', page });\n return requestAPI<ApiResponse<Question>>('questions', 'GET', undefined, params);\n },\n\n getNewestQuestions: (page?: number) => {\n const params = buildQuestionParams({ sort: 'creation', order: 'desc', page });\n return requestAPI<ApiResponse<Question>>('questions', 'GET', undefined, params);\n },\n\n getTopScoredQuestions: (page?: number) => {\n const params = buildQuestionParams({ sort: 'score', order: 'desc', page });\n return requestAPI<ApiResponse<Question>>('questions', 'GET', undefined, params);\n },\n\n getUnansweredQuestions: (page?: number) => {\n const params = buildQuestionParams({ isAnswered: false, sort: 'creation', order: 'desc', page });\n return requestAPI<ApiResponse<Question>>('questions', 'GET', undefined, params);\n },\n };\n};"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useApi } from '@backstage/core-plugin-api';
|
|
2
|
+
import { stackoverflowteamsApiRef } from '../../api/StackOverflowAPI.esm.js';
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { Box, CircularProgress, Typography, Card, IconButton, CardContent, Avatar, Chip, Link } from '@mui/material';
|
|
5
|
+
import { LogoutIcon } from '../../icons/LogoutIcon.esm.js';
|
|
6
|
+
|
|
7
|
+
const StackOverflowMe = () => {
|
|
8
|
+
const stackOverflowTeamsApi = useApi(stackoverflowteamsApiRef);
|
|
9
|
+
const logout = async () => {
|
|
10
|
+
try {
|
|
11
|
+
const success = await stackOverflowTeamsApi.logout();
|
|
12
|
+
if (success) {
|
|
13
|
+
window.location.reload();
|
|
14
|
+
} else {
|
|
15
|
+
throw new Error("Logout failed.");
|
|
16
|
+
}
|
|
17
|
+
} catch (error2) {
|
|
18
|
+
throw new Error("Error during logout:", error2);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const [userData, setUserData] = useState(null);
|
|
22
|
+
const [loading, setLoading] = useState(true);
|
|
23
|
+
const [error, setError] = useState(null);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const fetchMe = async () => {
|
|
26
|
+
try {
|
|
27
|
+
const data = await stackOverflowTeamsApi.getMe();
|
|
28
|
+
setUserData(data);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
setError("Failed to load user data");
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
fetchMe();
|
|
36
|
+
}, [stackOverflowTeamsApi]);
|
|
37
|
+
if (loading) {
|
|
38
|
+
return /* @__PURE__ */ React.createElement(
|
|
39
|
+
Box,
|
|
40
|
+
{
|
|
41
|
+
display: "flex",
|
|
42
|
+
justifyContent: "center",
|
|
43
|
+
alignItems: "center",
|
|
44
|
+
width: "100%"
|
|
45
|
+
},
|
|
46
|
+
/* @__PURE__ */ React.createElement(CircularProgress, { color: "primary" })
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (error) {
|
|
50
|
+
return /* @__PURE__ */ React.createElement(
|
|
51
|
+
Box,
|
|
52
|
+
{
|
|
53
|
+
display: "flex",
|
|
54
|
+
justifyContent: "center",
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
width: "100%"
|
|
57
|
+
},
|
|
58
|
+
/* @__PURE__ */ React.createElement(Typography, { color: "error" }, error)
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (!userData) return null;
|
|
62
|
+
return /* @__PURE__ */ React.createElement(
|
|
63
|
+
Box,
|
|
64
|
+
{
|
|
65
|
+
display: "flex",
|
|
66
|
+
justifyContent: "center",
|
|
67
|
+
alignItems: "center",
|
|
68
|
+
width: "100%"
|
|
69
|
+
},
|
|
70
|
+
/* @__PURE__ */ React.createElement(
|
|
71
|
+
Card,
|
|
72
|
+
{
|
|
73
|
+
variant: "outlined",
|
|
74
|
+
sx: {
|
|
75
|
+
p: 3,
|
|
76
|
+
position: "relative",
|
|
77
|
+
boxShadow: 3
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
/* @__PURE__ */ React.createElement(Box, { sx: { position: "absolute", top: 8, right: 8 } }, /* @__PURE__ */ React.createElement(
|
|
81
|
+
IconButton,
|
|
82
|
+
{
|
|
83
|
+
onClick: logout
|
|
84
|
+
},
|
|
85
|
+
/* @__PURE__ */ React.createElement(LogoutIcon, null)
|
|
86
|
+
)),
|
|
87
|
+
/* @__PURE__ */ React.createElement(CardContent, { sx: { textAlign: "center" } }, /* @__PURE__ */ React.createElement(Avatar, { src: userData.avatarUrl, sx: { mx: "auto", mb: 2 } }), /* @__PURE__ */ React.createElement(Typography, { variant: "h6", fontWeight: "bold", gutterBottom: true }, userData.name), /* @__PURE__ */ React.createElement(Box, { display: "flex", justifyContent: "center", gap: 1, mb: 1 }, /* @__PURE__ */ React.createElement(
|
|
88
|
+
Chip,
|
|
89
|
+
{
|
|
90
|
+
label: `Reputation: ${userData.reputation}`,
|
|
91
|
+
size: "small",
|
|
92
|
+
color: "primary"
|
|
93
|
+
}
|
|
94
|
+
), /* @__PURE__ */ React.createElement(Chip, { label: userData.role, size: "small", variant: "outlined" })), userData.jobTitle && /* @__PURE__ */ React.createElement(Typography, { variant: "body2", fontWeight: "500", mt: 1 }, "Position: ", userData.jobTitle), /* @__PURE__ */ React.createElement(
|
|
95
|
+
Link,
|
|
96
|
+
{
|
|
97
|
+
href: userData.webUrl,
|
|
98
|
+
target: "_blank",
|
|
99
|
+
rel: "noopener",
|
|
100
|
+
color: "primary",
|
|
101
|
+
sx: { display: "block", mt: 2, fontWeight: "bold" }
|
|
102
|
+
},
|
|
103
|
+
"View Profile"
|
|
104
|
+
))
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export { StackOverflowMe };
|
|
110
|
+
//# sourceMappingURL=StackOverflowMe.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StackOverflowMe.esm.js","sources":["../../../src/components/StackOverflow/StackOverflowMe.tsx"],"sourcesContent":["import { useApi } from '@backstage/core-plugin-api';\nimport { stackoverflowteamsApiRef, User } from '../../api';\nimport { useEffect, useState } from 'react';\nimport React from 'react';\n// eslint-disable-next-line no-restricted-imports\nimport {\n Box,\n Avatar,\n Typography,\n Card,\n CardContent,\n Link,\n CircularProgress,\n Chip,\n IconButton,\n} from '@mui/material';\nimport { LogoutIcon } from '../../icons';\n\nexport const StackOverflowMe = () => {\n const stackOverflowTeamsApi = useApi(stackoverflowteamsApiRef);\n const logout = async () => {\n try {\n const success = await stackOverflowTeamsApi.logout();\n if (success) {\n window.location.reload();\n } else {\n throw new Error('Logout failed.');\n }\n } catch (error:any) {\n throw new Error('Error during logout:', error);\n }\n };\n\n const [userData, setUserData] = useState<User | null>(null);\n const [loading, setLoading] = useState<Boolean>(true);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const fetchMe = async () => {\n try {\n const data = await stackOverflowTeamsApi.getMe();\n setUserData(data);\n } catch (err) {\n setError('Failed to load user data');\n } finally {\n setLoading(false);\n }\n };\n\n fetchMe();\n }, [stackOverflowTeamsApi]);\n\n if (loading) {\n return (\n <Box\n display=\"flex\"\n justifyContent=\"center\"\n alignItems=\"center\"\n width=\"100%\"\n >\n <CircularProgress color=\"primary\" />\n </Box>\n );\n }\n\n if (error) {\n return (\n <Box\n display=\"flex\"\n justifyContent=\"center\"\n alignItems=\"center\"\n width=\"100%\"\n >\n <Typography color=\"error\">{error}</Typography>\n </Box>\n );\n }\n\n if (!userData) return null;\n\n return (\n <Box\n display=\"flex\"\n justifyContent=\"center\"\n alignItems=\"center\"\n width=\"100%\"\n >\n <Card\n variant=\"outlined\"\n sx={{\n p: 3,\n position: 'relative',\n boxShadow: 3,\n }}\n >\n {/* Logout Button */}\n <Box sx={{ position: 'absolute', top: 8, right: 8 }}>\n <IconButton\n \n onClick={logout}\n >\n <LogoutIcon />\n </IconButton>\n </Box>\n <CardContent sx={{ textAlign: 'center' }}>\n {/* Avatar */}\n <Avatar src={userData.avatarUrl} sx={{ mx: 'auto', mb: 2 }} />\n\n {/* User Details */}\n <Typography variant=\"h6\" fontWeight=\"bold\" gutterBottom>\n {userData.name}\n </Typography>\n\n <Box display=\"flex\" justifyContent=\"center\" gap={1} mb={1}>\n <Chip\n label={`Reputation: ${userData.reputation}`}\n size=\"small\"\n color=\"primary\"\n />\n <Chip label={userData.role} size=\"small\" variant=\"outlined\" />\n </Box>\n\n {userData.jobTitle && (\n <Typography variant=\"body2\" fontWeight=\"500\" mt={1}>\n Position: {userData.jobTitle}\n </Typography>\n )}\n\n {/* Profile Link */}\n <Link\n href={userData.webUrl}\n target=\"_blank\"\n rel=\"noopener\"\n color=\"primary\"\n sx={{ display: 'block', mt: 2, fontWeight: 'bold' }}\n >\n View Profile\n </Link>\n </CardContent>\n </Card>\n </Box>\n );\n};\n"],"names":["error"],"mappings":"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"}
|