github-issue-tower-defence-management 1.85.0 → 1.87.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/.eslintrc.cjs +5 -1
- package/.github/workflows/console-ui.yml +47 -0
- package/.prettierignore +3 -0
- package/CHANGELOG.md +15 -0
- package/README.md +12 -0
- package/bin/adapter/entry-points/cli/index.js +37 -0
- package/bin/adapter/entry-points/cli/index.js.map +1 -1
- package/bin/adapter/entry-points/cli/projectConfig.js +2 -0
- package/bin/adapter/entry-points/cli/projectConfig.js.map +1 -1
- package/bin/adapter/entry-points/console/consoleServer.js +204 -0
- package/bin/adapter/entry-points/console/consoleServer.js.map +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +1 -0
- package/bin/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +49 -0
- package/bin/adapter/entry-points/console/ui-dist/index.html +13 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js +306 -0
- package/bin/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.js.map +1 -1
- package/package.json +22 -2
- package/scripts/copyConsoleUiDist.mjs +35 -0
- package/src/adapter/entry-points/cli/index.test.ts +126 -0
- package/src/adapter/entry-points/cli/index.ts +66 -0
- package/src/adapter/entry-points/cli/projectConfig.ts +4 -0
- package/src/adapter/entry-points/console/consoleServer.test.ts +297 -0
- package/src/adapter/entry-points/console/consoleServer.ts +220 -0
- package/src/adapter/entry-points/console/ui/.storybook/main.ts +12 -0
- package/src/adapter/entry-points/console/ui/.storybook/preview.ts +15 -0
- package/src/adapter/entry-points/console/ui/biome.json +47 -0
- package/src/adapter/entry-points/console/ui/components.json +20 -0
- package/src/adapter/entry-points/console/ui/index.html +12 -0
- package/src/adapter/entry-points/console/ui/src/components/ui/badge.stories.tsx +35 -0
- package/src/adapter/entry-points/console/ui/src/components/ui/badge.tsx +28 -0
- package/src/adapter/entry-points/console/ui/src/components/ui/button.stories.tsx +34 -0
- package/src/adapter/entry-points/console/ui/src/components/ui/button.tsx +50 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.stories.tsx +44 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx +58 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.stories.tsx +34 -0
- package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleTabBar.tsx +32 -0
- package/src/adapter/entry-points/console/ui/src/features/console/fixtures.ts +47 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleList.ts +65 -0
- package/src/adapter/entry-points/console/ui/src/features/console/hooks/useConsoleToken.ts +64 -0
- package/src/adapter/entry-points/console/ui/src/features/console/pages/ConsolePage.tsx +19 -0
- package/src/adapter/entry-points/console/ui/src/features/console/types.ts +69 -0
- package/src/adapter/entry-points/console/ui/src/index.css +31 -0
- package/src/adapter/entry-points/console/ui/src/lib/utils.ts +4 -0
- package/src/adapter/entry-points/console/ui/src/main.tsx +15 -0
- package/src/adapter/entry-points/console/ui/src/vite-env.d.ts +1 -0
- package/src/adapter/entry-points/console/ui/tsconfig.json +24 -0
- package/src/adapter/entry-points/console/ui/vite.config.ts +19 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-DFxrSRH4.css +1 -0
- package/src/adapter/entry-points/console/ui-dist/assets/index-DcOZ02ON.js +49 -0
- package/src/adapter/entry-points/console/ui-dist/index.html +13 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.test.ts +630 -0
- package/src/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.ts +492 -0
- package/src/domain/usecases/adapter-interfaces/IssueRepository.ts +51 -0
- package/tsconfig.build.json +7 -1
- package/tsconfig.json +6 -1
- package/types/adapter/entry-points/cli/index.d.ts.map +1 -1
- package/types/adapter/entry-points/cli/projectConfig.d.ts +1 -0
- package/types/adapter/entry-points/cli/projectConfig.d.ts.map +1 -1
- package/types/adapter/entry-points/console/consoleServer.d.ts +19 -0
- package/types/adapter/entry-points/console/consoleServer.d.ts.map +1 -0
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts +18 -1
- package/types/adapter/repositories/issue/ApiV3CheerioRestIssueRepository.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts +47 -0
- package/types/domain/usecases/adapter-interfaces/IssueRepository.d.ts.map +1 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import * as http from 'http';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_CONSOLE_PORT = 9981;
|
|
6
|
+
|
|
7
|
+
export const CONSOLE_TOKEN_HEADER = 'x-pv-token';
|
|
8
|
+
|
|
9
|
+
const PLACEHOLDER_INDEX_HTML = `<!DOCTYPE html>
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="utf-8" />
|
|
13
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
14
|
+
<title>TDPM Console</title>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<main>
|
|
18
|
+
<h1>TDPM Console</h1>
|
|
19
|
+
<p>The console UI bundle has not been built yet.</p>
|
|
20
|
+
</main>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const MIME_TYPES: Record<string, string> = {
|
|
26
|
+
'.html': 'text/html; charset=utf-8',
|
|
27
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
28
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
29
|
+
'.css': 'text/css; charset=utf-8',
|
|
30
|
+
'.json': 'application/json; charset=utf-8',
|
|
31
|
+
'.svg': 'image/svg+xml',
|
|
32
|
+
'.png': 'image/png',
|
|
33
|
+
'.jpg': 'image/jpeg',
|
|
34
|
+
'.jpeg': 'image/jpeg',
|
|
35
|
+
'.gif': 'image/gif',
|
|
36
|
+
'.ico': 'image/x-icon',
|
|
37
|
+
'.map': 'application/json; charset=utf-8',
|
|
38
|
+
'.woff': 'font/woff',
|
|
39
|
+
'.woff2': 'font/woff2',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const hasDotSegment = (requestPath: string): boolean =>
|
|
43
|
+
requestPath
|
|
44
|
+
.split('/')
|
|
45
|
+
.some((segment) => segment.length > 0 && segment.startsWith('.'));
|
|
46
|
+
|
|
47
|
+
export const requiresToken = (requestPath: string): boolean =>
|
|
48
|
+
requestPath.startsWith('/api/') ||
|
|
49
|
+
requestPath === '/api' ||
|
|
50
|
+
requestPath.endsWith('.json');
|
|
51
|
+
|
|
52
|
+
export const isTokenValid = (
|
|
53
|
+
expectedToken: string,
|
|
54
|
+
providedToken: string | null,
|
|
55
|
+
): boolean => providedToken !== null && providedToken === expectedToken;
|
|
56
|
+
|
|
57
|
+
export const extractProvidedToken = (
|
|
58
|
+
queryToken: string | string[] | null,
|
|
59
|
+
headerToken: string | string[] | undefined,
|
|
60
|
+
): string | null => {
|
|
61
|
+
if (typeof queryToken === 'string' && queryToken.length > 0) {
|
|
62
|
+
return queryToken;
|
|
63
|
+
}
|
|
64
|
+
if (typeof headerToken === 'string' && headerToken.length > 0) {
|
|
65
|
+
return headerToken;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const contentTypeForPath = (filePath: string): string => {
|
|
71
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
72
|
+
return MIME_TYPES[extension] ?? 'application/octet-stream';
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const resolveStaticFilePath = (
|
|
76
|
+
uiDistDir: string,
|
|
77
|
+
requestPath: string,
|
|
78
|
+
): string | null => {
|
|
79
|
+
const relativePath = requestPath === '/' ? '/index.html' : requestPath;
|
|
80
|
+
const normalized = path
|
|
81
|
+
.normalize(relativePath)
|
|
82
|
+
.replace(/^(\.\.(\/|\\|$))+/, '');
|
|
83
|
+
const candidate = path.join(uiDistDir, normalized);
|
|
84
|
+
const resolvedRoot = path.resolve(uiDistDir);
|
|
85
|
+
const resolvedCandidate = path.resolve(candidate);
|
|
86
|
+
if (
|
|
87
|
+
resolvedCandidate !== resolvedRoot &&
|
|
88
|
+
!resolvedCandidate.startsWith(resolvedRoot + path.sep)
|
|
89
|
+
) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return resolvedCandidate;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const readStaticFile = (filePath: string): Buffer | null => {
|
|
96
|
+
try {
|
|
97
|
+
const stat = fs.statSync(filePath);
|
|
98
|
+
if (!stat.isFile()) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return fs.readFileSync(filePath);
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type ConsoleServerOptions = {
|
|
108
|
+
accessToken: string;
|
|
109
|
+
uiDistDir: string;
|
|
110
|
+
consoleDataOutputDir: string | null;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const sendNotFound = (response: http.ServerResponse): void => {
|
|
114
|
+
response.writeHead(404, {
|
|
115
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
116
|
+
'Cache-Control': 'no-store',
|
|
117
|
+
});
|
|
118
|
+
response.end('Not Found');
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const sendUnauthorized = (response: http.ServerResponse): void => {
|
|
122
|
+
response.writeHead(401, {
|
|
123
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
124
|
+
'Cache-Control': 'no-store',
|
|
125
|
+
});
|
|
126
|
+
response.end('Unauthorized');
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const serveBootstrapIndex = (response: http.ServerResponse): void => {
|
|
130
|
+
response.writeHead(200, {
|
|
131
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
132
|
+
'Cache-Control': 'no-store',
|
|
133
|
+
});
|
|
134
|
+
response.end(PLACEHOLDER_INDEX_HTML);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export const handleConsoleRequest = (
|
|
138
|
+
options: ConsoleServerOptions,
|
|
139
|
+
request: http.IncomingMessage,
|
|
140
|
+
response: http.ServerResponse,
|
|
141
|
+
): void => {
|
|
142
|
+
const requestUrl = new URL(request.url ?? '/', 'http://localhost');
|
|
143
|
+
const requestPath = requestUrl.pathname;
|
|
144
|
+
|
|
145
|
+
if (hasDotSegment(requestPath)) {
|
|
146
|
+
sendNotFound(response);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (requiresToken(requestPath)) {
|
|
151
|
+
const providedToken = extractProvidedToken(
|
|
152
|
+
requestUrl.searchParams.get('k'),
|
|
153
|
+
request.headers[CONSOLE_TOKEN_HEADER],
|
|
154
|
+
);
|
|
155
|
+
if (!isTokenValid(options.accessToken, providedToken)) {
|
|
156
|
+
sendUnauthorized(response);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
sendNotFound(response);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (requestPath === '/' || requestPath === '/index.html') {
|
|
164
|
+
const indexFilePath = resolveStaticFilePath(
|
|
165
|
+
options.uiDistDir,
|
|
166
|
+
'/index.html',
|
|
167
|
+
);
|
|
168
|
+
const indexContent =
|
|
169
|
+
indexFilePath === null ? null : readStaticFile(indexFilePath);
|
|
170
|
+
if (indexContent === null) {
|
|
171
|
+
serveBootstrapIndex(response);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
response.writeHead(200, {
|
|
175
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
176
|
+
'Cache-Control': 'no-store',
|
|
177
|
+
});
|
|
178
|
+
response.end(indexContent);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const staticFilePath = resolveStaticFilePath(options.uiDistDir, requestPath);
|
|
183
|
+
if (staticFilePath === null) {
|
|
184
|
+
sendNotFound(response);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const staticContent = readStaticFile(staticFilePath);
|
|
188
|
+
if (staticContent === null) {
|
|
189
|
+
sendNotFound(response);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
response.writeHead(200, {
|
|
193
|
+
'Content-Type': contentTypeForPath(staticFilePath),
|
|
194
|
+
'Cache-Control': 'no-store',
|
|
195
|
+
});
|
|
196
|
+
response.end(staticContent);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const createConsoleServer = (
|
|
200
|
+
options: ConsoleServerOptions,
|
|
201
|
+
): http.Server =>
|
|
202
|
+
http.createServer((request, response) => {
|
|
203
|
+
handleConsoleRequest(options, request, response);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
export type StartConsoleServerOptions = ConsoleServerOptions & {
|
|
207
|
+
port: number;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export const startConsoleServer = (
|
|
211
|
+
options: StartConsoleServerOptions,
|
|
212
|
+
): Promise<http.Server> =>
|
|
213
|
+
new Promise((resolve, reject) => {
|
|
214
|
+
const server = createConsoleServer(options);
|
|
215
|
+
server.once('error', reject);
|
|
216
|
+
server.listen(options.port, () => {
|
|
217
|
+
server.removeListener('error', reject);
|
|
218
|
+
resolve(server);
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StorybookConfig } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
const config: StorybookConfig = {
|
|
4
|
+
stories: ['../src/**/*.stories.@(ts|tsx)'],
|
|
5
|
+
addons: [],
|
|
6
|
+
framework: {
|
|
7
|
+
name: '@storybook/react-vite',
|
|
8
|
+
options: {},
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default config;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Preview } from '@storybook/react-vite';
|
|
2
|
+
import '../src/index.css';
|
|
3
|
+
|
|
4
|
+
const preview: Preview = {
|
|
5
|
+
parameters: {
|
|
6
|
+
controls: {
|
|
7
|
+
matchers: {
|
|
8
|
+
color: /(background|color)$/i,
|
|
9
|
+
date: /Date$/,
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default preview;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
|
|
3
|
+
"root": false,
|
|
4
|
+
"vcs": {
|
|
5
|
+
"enabled": false,
|
|
6
|
+
"clientKind": "git",
|
|
7
|
+
"useIgnoreFile": false
|
|
8
|
+
},
|
|
9
|
+
"files": {
|
|
10
|
+
"ignoreUnknown": false,
|
|
11
|
+
"includes": ["src/**", ".storybook/**"]
|
|
12
|
+
},
|
|
13
|
+
"formatter": {
|
|
14
|
+
"enabled": true,
|
|
15
|
+
"indentStyle": "space",
|
|
16
|
+
"indentWidth": 2,
|
|
17
|
+
"lineWidth": 80
|
|
18
|
+
},
|
|
19
|
+
"linter": {
|
|
20
|
+
"enabled": true,
|
|
21
|
+
"rules": {
|
|
22
|
+
"recommended": true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"javascript": {
|
|
26
|
+
"formatter": {
|
|
27
|
+
"quoteStyle": "single",
|
|
28
|
+
"semicolons": "always",
|
|
29
|
+
"trailingCommas": "all"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"css": {
|
|
33
|
+
"parser": {
|
|
34
|
+
"tailwindDirectives": true
|
|
35
|
+
},
|
|
36
|
+
"formatter": {
|
|
37
|
+
"quoteStyle": "double"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"assist": {
|
|
41
|
+
"actions": {
|
|
42
|
+
"source": {
|
|
43
|
+
"organizeImports": "on"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": false,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/index.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/components",
|
|
15
|
+
"utils": "@/lib/utils",
|
|
16
|
+
"ui": "@/components/ui",
|
|
17
|
+
"lib": "@/lib",
|
|
18
|
+
"hooks": "@/features/console/hooks"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>TDPM Console</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Badge } from './badge';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Badge> = {
|
|
5
|
+
title: 'UI/Badge',
|
|
6
|
+
component: Badge,
|
|
7
|
+
args: {
|
|
8
|
+
children: 'PR',
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
|
|
14
|
+
type Story = StoryObj<typeof Badge>;
|
|
15
|
+
|
|
16
|
+
export const Pr: Story = {
|
|
17
|
+
args: {
|
|
18
|
+
variant: 'default',
|
|
19
|
+
children: 'PR',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Issue: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
variant: 'secondary',
|
|
26
|
+
children: 'Issue',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Outline: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
variant: 'outline',
|
|
33
|
+
children: 'story: TDPM Console port',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
const badgeVariants = cva(
|
|
6
|
+
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors',
|
|
7
|
+
{
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: 'border-transparent bg-primary text-primary-foreground',
|
|
11
|
+
secondary: 'border-transparent bg-secondary text-secondary-foreground',
|
|
12
|
+
outline: 'text-foreground',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
variant: 'default',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
export type BadgeProps = React.HTMLAttributes<HTMLDivElement> &
|
|
22
|
+
VariantProps<typeof badgeVariants>;
|
|
23
|
+
|
|
24
|
+
export const Badge = ({ className, variant, ...props }: BadgeProps) => (
|
|
25
|
+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
export { badgeVariants };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Button } from './button';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof Button> = {
|
|
5
|
+
title: 'UI/Button',
|
|
6
|
+
component: Button,
|
|
7
|
+
args: {
|
|
8
|
+
children: 'Awaiting Quality Check',
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
|
|
14
|
+
type Story = StoryObj<typeof Button>;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {};
|
|
17
|
+
|
|
18
|
+
export const Ghost: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
variant: 'ghost',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const Outline: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
variant: 'outline',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Small: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
size: 'sm',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Slot } from '@radix-ui/react-slot';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
const buttonVariants = cva(
|
|
7
|
+
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
|
8
|
+
{
|
|
9
|
+
variants: {
|
|
10
|
+
variant: {
|
|
11
|
+
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
|
12
|
+
outline:
|
|
13
|
+
'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
|
|
14
|
+
secondary:
|
|
15
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
|
16
|
+
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
|
17
|
+
},
|
|
18
|
+
size: {
|
|
19
|
+
default: 'h-9 px-4 py-2',
|
|
20
|
+
sm: 'h-8 rounded-md px-3 text-xs',
|
|
21
|
+
lg: 'h-10 rounded-md px-8',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
defaultVariants: {
|
|
25
|
+
variant: 'default',
|
|
26
|
+
size: 'default',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> &
|
|
32
|
+
VariantProps<typeof buttonVariants> & {
|
|
33
|
+
asChild?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
37
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
38
|
+
const Comp = asChild ? Slot : 'button';
|
|
39
|
+
return (
|
|
40
|
+
<Comp
|
|
41
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
42
|
+
ref={ref}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
Button.displayName = 'Button';
|
|
49
|
+
|
|
50
|
+
export { buttonVariants };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { consoleListItemsFixture } from '../fixtures';
|
|
3
|
+
import { ConsoleListView } from './ConsoleListView';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof ConsoleListView> = {
|
|
6
|
+
title: 'Console/ConsoleListView',
|
|
7
|
+
component: ConsoleListView,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
|
|
12
|
+
type Story = StoryObj<typeof ConsoleListView>;
|
|
13
|
+
|
|
14
|
+
export const WithItems: Story = {
|
|
15
|
+
args: {
|
|
16
|
+
items: consoleListItemsFixture,
|
|
17
|
+
isLoading: false,
|
|
18
|
+
error: null,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const Loading: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
items: [],
|
|
25
|
+
isLoading: true,
|
|
26
|
+
error: null,
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Empty: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
items: [],
|
|
33
|
+
isLoading: false,
|
|
34
|
+
error: null,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const ErrorState: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
items: [],
|
|
41
|
+
isLoading: false,
|
|
42
|
+
error: 'HTTP 404',
|
|
43
|
+
},
|
|
44
|
+
};
|
package/src/adapter/entry-points/console/ui/src/features/console/components/ConsoleListView.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Badge } from '@/components/ui/badge';
|
|
2
|
+
import type { ConsoleListItem } from '../types';
|
|
3
|
+
|
|
4
|
+
export type ConsoleListViewProps = {
|
|
5
|
+
items: ConsoleListItem[];
|
|
6
|
+
isLoading: boolean;
|
|
7
|
+
error: string | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const ConsoleListView = ({
|
|
11
|
+
items,
|
|
12
|
+
isLoading,
|
|
13
|
+
error,
|
|
14
|
+
}: ConsoleListViewProps) => {
|
|
15
|
+
if (error !== null) {
|
|
16
|
+
return (
|
|
17
|
+
<p role="alert" className="p-4 text-sm text-destructive">
|
|
18
|
+
Failed to load list: {error}
|
|
19
|
+
</p>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (isLoading) {
|
|
24
|
+
return <p className="p-4 text-sm text-muted-foreground">Loading list...</p>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (items.length === 0) {
|
|
28
|
+
return <p className="p-4 text-sm text-muted-foreground">No items.</p>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<ul className="divide-y divide-border">
|
|
33
|
+
{items.map((item) => (
|
|
34
|
+
<li key={item.itemId} className="flex flex-col gap-1 p-3">
|
|
35
|
+
<div className="flex items-center gap-2">
|
|
36
|
+
<Badge variant={item.isPr ? 'default' : 'secondary'}>
|
|
37
|
+
{item.isPr ? 'PR' : 'Issue'}
|
|
38
|
+
</Badge>
|
|
39
|
+
<a
|
|
40
|
+
href={item.url}
|
|
41
|
+
className="font-medium underline-offset-2 hover:underline"
|
|
42
|
+
>
|
|
43
|
+
{item.title}
|
|
44
|
+
</a>
|
|
45
|
+
<span className="text-sm text-muted-foreground">
|
|
46
|
+
#{item.number}
|
|
47
|
+
</span>
|
|
48
|
+
</div>
|
|
49
|
+
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
50
|
+
<span>{item.repo}</span>
|
|
51
|
+
{item.story !== '' && <span>story: {item.story}</span>}
|
|
52
|
+
<span>{new Date(item.createdAt).toISOString()}</span>
|
|
53
|
+
</div>
|
|
54
|
+
</li>
|
|
55
|
+
))}
|
|
56
|
+
</ul>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import type { ConsoleTabName } from '../types';
|
|
4
|
+
import { ConsoleTabBar } from './ConsoleTabBar';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof ConsoleTabBar> = {
|
|
7
|
+
title: 'Console/ConsoleTabBar',
|
|
8
|
+
component: ConsoleTabBar,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default meta;
|
|
12
|
+
|
|
13
|
+
type Story = StoryObj<typeof ConsoleTabBar>;
|
|
14
|
+
|
|
15
|
+
export const PrsActive: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
activeTab: 'prs',
|
|
18
|
+
onSelectTab: () => undefined,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const TriageActive: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
activeTab: 'triage',
|
|
25
|
+
onSelectTab: () => undefined,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Interactive: Story = {
|
|
30
|
+
render: () => {
|
|
31
|
+
const [activeTab, setActiveTab] = useState<ConsoleTabName>('prs');
|
|
32
|
+
return <ConsoleTabBar activeTab={activeTab} onSelectTab={setActiveTab} />;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Button } from '@/components/ui/button';
|
|
2
|
+
import { cn } from '@/lib/utils';
|
|
3
|
+
import { CONSOLE_TABS, type ConsoleTabName } from '../types';
|
|
4
|
+
|
|
5
|
+
export type ConsoleTabBarProps = {
|
|
6
|
+
activeTab: ConsoleTabName;
|
|
7
|
+
onSelectTab: (tab: ConsoleTabName) => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const ConsoleTabBar = ({
|
|
11
|
+
activeTab,
|
|
12
|
+
onSelectTab,
|
|
13
|
+
}: ConsoleTabBarProps) => (
|
|
14
|
+
<nav
|
|
15
|
+
aria-label="Console tabs"
|
|
16
|
+
className="flex flex-wrap gap-1 border-b border-border p-2"
|
|
17
|
+
>
|
|
18
|
+
{CONSOLE_TABS.map((tab) => (
|
|
19
|
+
<Button
|
|
20
|
+
key={tab.name}
|
|
21
|
+
type="button"
|
|
22
|
+
size="sm"
|
|
23
|
+
variant={tab.name === activeTab ? 'default' : 'ghost'}
|
|
24
|
+
aria-current={tab.name === activeTab ? 'page' : undefined}
|
|
25
|
+
className={cn(tab.name === activeTab && 'font-semibold')}
|
|
26
|
+
onClick={() => onSelectTab(tab.name)}
|
|
27
|
+
>
|
|
28
|
+
{tab.label}
|
|
29
|
+
</Button>
|
|
30
|
+
))}
|
|
31
|
+
</nav>
|
|
32
|
+
);
|