Compare commits
20 Commits
feat/feedb
...
main
Author | SHA1 | Date | |
---|---|---|---|
fc001b4dfb | |||
235c7c1ef4 | |||
0d0c4c8469 | |||
9847fdb70c | |||
82558ff06a | |||
32e5d3bb8d | |||
6c5f7c08b5 | |||
0787122c25 | |||
8309d3a92c | |||
f284f74734 | |||
33cfe91461 | |||
7f6a0f36e9 | |||
377942fe3c | |||
4046c31fbd | |||
fc125519b9 | |||
e8d790d799 | |||
618a2f67a4 | |||
5ae746346f | |||
154df360ac | |||
83b615d989 |
@ -2,7 +2,7 @@
|
|||||||
"name": "@boring.tools/api",
|
"name": "@boring.tools/api",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --hot src/index.ts",
|
"dev": "bun run --hot src/index.ts",
|
||||||
"build": "bun build --entrypoints ./src/index.ts --outdir ../../build/api --target bun --splitting --sourcemap=linked",
|
"build": "bun build --entrypoints ./src/index.ts --outdir ../../build/api --target bun --splitting",
|
||||||
"test": "bun test --preload ./src/index.ts"
|
"test": "bun test --preload ./src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
151
apps/api/src/access-token/access-token.test.ts
Normal file
151
apps/api/src/access-token/access-token.test.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
||||||
|
import { access_token, db, user } from '@boring.tools/database'
|
||||||
|
import type {
|
||||||
|
AccessTokenCreateInput,
|
||||||
|
AccessTokenListOutput,
|
||||||
|
AccessTokenOutput,
|
||||||
|
ChangelogCreateInput,
|
||||||
|
ChangelogCreateOutput,
|
||||||
|
ChangelogListOutput,
|
||||||
|
ChangelogOutput,
|
||||||
|
UserOutput,
|
||||||
|
} from '@boring.tools/schema'
|
||||||
|
import type { z } from '@hono/zod-openapi'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import { fetch } from '../utils/testing/fetch'
|
||||||
|
|
||||||
|
describe('AccessToken', () => {
|
||||||
|
let testUser: z.infer<typeof UserOutput>
|
||||||
|
let testAccessToken: z.infer<typeof AccessTokenOutput>
|
||||||
|
let createdAccessToken: z.infer<typeof AccessTokenOutput>
|
||||||
|
let testChangelog: z.infer<typeof ChangelogOutput>
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const createdUser = await db
|
||||||
|
.insert(user)
|
||||||
|
.values({ email: 'access_token@test.local', providerId: 'test_000' })
|
||||||
|
.returning()
|
||||||
|
const tAccessToken = await db
|
||||||
|
.insert(access_token)
|
||||||
|
.values({
|
||||||
|
token: '1234567890',
|
||||||
|
userId: createdUser[0].id,
|
||||||
|
name: 'testtoken',
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
testAccessToken = tAccessToken[0] as z.infer<typeof AccessTokenOutput>
|
||||||
|
testUser = createdUser[0] as z.infer<typeof UserOutput>
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await db.delete(user).where(eq(user.email, 'access_token@test.local'))
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Create', () => {
|
||||||
|
test('Success', async () => {
|
||||||
|
const payload: z.infer<typeof AccessTokenCreateInput> = {
|
||||||
|
name: 'Test Token',
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
{
|
||||||
|
path: '/v1/access-token',
|
||||||
|
method: 'POST',
|
||||||
|
body: payload,
|
||||||
|
},
|
||||||
|
testAccessToken.token as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
const json = (await res.json()) as z.infer<typeof AccessTokenOutput>
|
||||||
|
|
||||||
|
createdAccessToken = json
|
||||||
|
expect(res.status).toBe(201)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// describe('By Id', () => {
|
||||||
|
// test('Success', async () => {
|
||||||
|
// const res = await fetch(
|
||||||
|
// {
|
||||||
|
// path: `/v1/changelog/${testChangelog.id}`,
|
||||||
|
// method: 'GET',
|
||||||
|
// },
|
||||||
|
// testAccessToken.token as string,
|
||||||
|
// )
|
||||||
|
|
||||||
|
// expect(res.status).toBe(200)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test('Not Found', async () => {
|
||||||
|
// const res = await fetch(
|
||||||
|
// {
|
||||||
|
// path: '/v1/changelog/635f4aa7-79fc-4d6b-af7d-6731999cc8bb',
|
||||||
|
// method: 'GET',
|
||||||
|
// },
|
||||||
|
// testAccessToken.token as string,
|
||||||
|
// )
|
||||||
|
|
||||||
|
// expect(res.status).toBe(404)
|
||||||
|
// })
|
||||||
|
|
||||||
|
// test('Invalid Id', async () => {
|
||||||
|
// const res = await fetch(
|
||||||
|
// {
|
||||||
|
// path: '/v1/changelog/some',
|
||||||
|
// method: 'GET',
|
||||||
|
// },
|
||||||
|
// testAccessToken.token as string,
|
||||||
|
// )
|
||||||
|
|
||||||
|
// expect(res.status).toBe(400)
|
||||||
|
|
||||||
|
// const json = (await res.json()) as { success: boolean }
|
||||||
|
// expect(json.success).toBeFalse()
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
describe('List', () => {
|
||||||
|
test('Success', async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
{
|
||||||
|
path: '/v1/access-token',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
testAccessToken.token as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
|
||||||
|
const json = (await res.json()) as z.infer<typeof AccessTokenListOutput>
|
||||||
|
// Check if token is redacted
|
||||||
|
expect(json[0].token).toHaveLength(10)
|
||||||
|
expect(json).toHaveLength(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Remove', () => {
|
||||||
|
test('Success', async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
{
|
||||||
|
path: `/v1/access-token/${createdAccessToken.id}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
},
|
||||||
|
testAccessToken.token as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Not found', async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
{
|
||||||
|
path: `/v1/access-token/${createdAccessToken.id}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
},
|
||||||
|
testAccessToken.token as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(404)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -29,7 +29,7 @@ export const registerAccessTokenDelete = (api: typeof accessTokenApi) => {
|
|||||||
const id = c.req.param('id')
|
const id = c.req.param('id')
|
||||||
const userId = await verifyAuthentication(c)
|
const userId = await verifyAuthentication(c)
|
||||||
|
|
||||||
const result = await db
|
const [result] = await db
|
||||||
.delete(access_token)
|
.delete(access_token)
|
||||||
.where(and(eq(access_token.userId, userId), eq(access_token.id, id)))
|
.where(and(eq(access_token.userId, userId), eq(access_token.id, id)))
|
||||||
.returning()
|
.returning()
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import type { UserOutput } from '@boring.tools/schema'
|
import type { UserOutput } from '@boring.tools/schema'
|
||||||
import { sentry } from '@hono/sentry'
|
// import { sentry } from '@hono/sentry'
|
||||||
import { OpenAPIHono, type z } from '@hono/zod-openapi'
|
import { OpenAPIHono, type z } from '@hono/zod-openapi'
|
||||||
import { apiReference } from '@scalar/hono-api-reference'
|
import { apiReference } from '@scalar/hono-api-reference'
|
||||||
import { cors } from 'hono/cors'
|
import { cors } from 'hono/cors'
|
||||||
|
import { requestId } from 'hono/request-id'
|
||||||
|
|
||||||
import changelog from './changelog'
|
import changelog from './changelog'
|
||||||
import user from './user'
|
|
||||||
|
|
||||||
import { accessTokenApi } from './access-token'
|
import { accessTokenApi } from './access-token'
|
||||||
import pageApi from './page'
|
import pageApi from './page'
|
||||||
import statisticApi from './statistic'
|
import statisticApi from './statistic'
|
||||||
|
import userApi from './user'
|
||||||
import { authentication } from './utils/authentication'
|
import { authentication } from './utils/authentication'
|
||||||
import { handleError, handleZodError } from './utils/errors'
|
import { handleError, handleZodError } from './utils/errors'
|
||||||
|
import { logger } from './utils/logger'
|
||||||
import { startup } from './utils/startup'
|
import { startup } from './utils/startup'
|
||||||
|
|
||||||
type User = z.infer<typeof UserOutput>
|
type User = z.infer<typeof UserOutput>
|
||||||
@ -31,9 +33,12 @@ export const app = new OpenAPIHono<{ Variables: Variables }>({
|
|||||||
// dsn: 'https://1d7428bbab0a305078cf4aa380721aa2@o4508167321354240.ingest.de.sentry.io/4508167323648080',
|
// dsn: 'https://1d7428bbab0a305078cf4aa380721aa2@o4508167321354240.ingest.de.sentry.io/4508167323648080',
|
||||||
// }),
|
// }),
|
||||||
// )
|
// )
|
||||||
|
|
||||||
app.onError(handleError)
|
app.onError(handleError)
|
||||||
app.use('*', cors())
|
app.use('*', cors())
|
||||||
app.use('/v1/*', authentication)
|
app.use('/v1/*', authentication)
|
||||||
|
app.use('*', requestId())
|
||||||
|
app.use(logger())
|
||||||
app.openAPIRegistry.registerComponent('securitySchemes', 'AccessToken', {
|
app.openAPIRegistry.registerComponent('securitySchemes', 'AccessToken', {
|
||||||
type: 'http',
|
type: 'http',
|
||||||
scheme: 'bearer',
|
scheme: 'bearer',
|
||||||
@ -43,7 +48,7 @@ app.openAPIRegistry.registerComponent('securitySchemes', 'Clerk', {
|
|||||||
scheme: 'bearer',
|
scheme: 'bearer',
|
||||||
})
|
})
|
||||||
|
|
||||||
app.route('/v1/user', user)
|
app.route('/v1/user', userApi)
|
||||||
app.route('/v1/changelog', changelog)
|
app.route('/v1/changelog', changelog)
|
||||||
app.route('/v1/page', pageApi)
|
app.route('/v1/page', pageApi)
|
||||||
app.route('/v1/access-token', accessTokenApi)
|
app.route('/v1/access-token', accessTokenApi)
|
||||||
|
@ -20,7 +20,7 @@ const route = createRoute({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
201: {
|
||||||
content: {
|
content: {
|
||||||
'application/json': {
|
'application/json': {
|
||||||
schema: PageOutput,
|
schema: PageOutput,
|
||||||
@ -62,6 +62,6 @@ export const registerPageCreate = (api: typeof pageApi) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.json(PageOutput.parse(result), 200)
|
return c.json(PageOutput.parse(result), 201)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ export const registerPageDelete = (api: typeof pageApi) => {
|
|||||||
const userId = await verifyAuthentication(c)
|
const userId = await verifyAuthentication(c)
|
||||||
const { id } = c.req.valid('param')
|
const { id } = c.req.valid('param')
|
||||||
|
|
||||||
const result = await db
|
const [result] = await db
|
||||||
.delete(page)
|
.delete(page)
|
||||||
.where(and(eq(page.userId, userId), eq(page.id, id)))
|
.where(and(eq(page.userId, userId), eq(page.id, id)))
|
||||||
.returning()
|
.returning()
|
||||||
|
171
apps/api/src/page/page.test.ts
Normal file
171
apps/api/src/page/page.test.ts
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
||||||
|
import { access_token, db, user } from '@boring.tools/database'
|
||||||
|
import type {
|
||||||
|
AccessTokenCreateInput,
|
||||||
|
AccessTokenListOutput,
|
||||||
|
AccessTokenOutput,
|
||||||
|
ChangelogCreateInput,
|
||||||
|
ChangelogCreateOutput,
|
||||||
|
ChangelogListOutput,
|
||||||
|
ChangelogOutput,
|
||||||
|
PageCreateInput,
|
||||||
|
PageListOutput,
|
||||||
|
PageOutput,
|
||||||
|
PageUpdateInput,
|
||||||
|
UserOutput,
|
||||||
|
} from '@boring.tools/schema'
|
||||||
|
import type { z } from '@hono/zod-openapi'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import { fetch } from '../utils/testing/fetch'
|
||||||
|
|
||||||
|
describe('Page', () => {
|
||||||
|
let testUser: z.infer<typeof UserOutput>
|
||||||
|
let testAccessToken: z.infer<typeof AccessTokenOutput>
|
||||||
|
let createdAccessToken: z.infer<typeof AccessTokenOutput>
|
||||||
|
let testPage: z.infer<typeof PageOutput>
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const createdUser = await db
|
||||||
|
.insert(user)
|
||||||
|
.values({ email: 'page@test.local', providerId: 'test_000' })
|
||||||
|
.returning()
|
||||||
|
const tAccessToken = await db
|
||||||
|
.insert(access_token)
|
||||||
|
.values({
|
||||||
|
token: '1234567890',
|
||||||
|
userId: createdUser[0].id,
|
||||||
|
name: 'testtoken',
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
testAccessToken = tAccessToken[0] as z.infer<typeof AccessTokenOutput>
|
||||||
|
testUser = createdUser[0] as z.infer<typeof UserOutput>
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await db.delete(user).where(eq(user.email, 'page@test.local'))
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Create', () => {
|
||||||
|
test('Success', async () => {
|
||||||
|
const payload: z.infer<typeof PageCreateInput> = {
|
||||||
|
title: 'Test Page',
|
||||||
|
changelogIds: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
{
|
||||||
|
path: '/v1/page',
|
||||||
|
method: 'POST',
|
||||||
|
body: payload,
|
||||||
|
},
|
||||||
|
testAccessToken.token as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
const json = (await res.json()) as z.infer<typeof PageOutput>
|
||||||
|
testPage = json
|
||||||
|
expect(res.status).toBe(201)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('By Id', () => {
|
||||||
|
test('Success', async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
{
|
||||||
|
path: `/v1/page/${testPage.id}`,
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
testAccessToken.token as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Not Found', async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
{
|
||||||
|
path: '/v1/page/635f4aa7-79fc-4d6b-af7d-6731999cc8bb',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
testAccessToken.token as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(404)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Update', () => {
|
||||||
|
test('Success', async () => {
|
||||||
|
const update: z.infer<typeof PageUpdateInput> = {
|
||||||
|
title: 'Test Update',
|
||||||
|
}
|
||||||
|
const res = await fetch(
|
||||||
|
{
|
||||||
|
path: `/v1/page/${testPage.id}`,
|
||||||
|
method: 'PUT',
|
||||||
|
body: update,
|
||||||
|
},
|
||||||
|
testAccessToken.token as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Public', () => {
|
||||||
|
test('Success', async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
{
|
||||||
|
path: `/v1/page/${testPage.id}/public`,
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
testAccessToken.token as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('List', () => {
|
||||||
|
test('Success', async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
{
|
||||||
|
path: '/v1/page',
|
||||||
|
method: 'GET',
|
||||||
|
},
|
||||||
|
testAccessToken.token as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
|
||||||
|
const json = (await res.json()) as z.infer<typeof PageListOutput>
|
||||||
|
// Check if token is redacted
|
||||||
|
expect(json).toHaveLength(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Remove', () => {
|
||||||
|
test('Success', async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
{
|
||||||
|
path: `/v1/page/${testPage.id}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
},
|
||||||
|
testAccessToken.token as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Not found', async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
{
|
||||||
|
path: `/v1/page/${testPage.id}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
},
|
||||||
|
testAccessToken.token as string,
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(res.status).toBe(404)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -91,6 +91,7 @@ export const registerPagePublic = (api: typeof pageApi) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redis.set(id, JSON.stringify(mappedResult), { EX: 60 })
|
redis.set(id, JSON.stringify(mappedResult), { EX: 60 })
|
||||||
return c.json(PagePublicOutput.parse(mappedResult), 200)
|
const asd = PagePublicOutput.parse(mappedResult)
|
||||||
|
return c.json(asd, 200)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { createRoute } from '@hono/zod-openapi'
|
|||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
|
|
||||||
import type { userApi } from '.'
|
import type { userApi } from '.'
|
||||||
|
import { verifyAuthentication } from '../utils/authentication'
|
||||||
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
import { openApiErrorResponses, openApiSecurity } from '../utils/openapi'
|
||||||
|
|
||||||
const route = createRoute({
|
const route = createRoute({
|
||||||
@ -24,9 +25,9 @@ const route = createRoute({
|
|||||||
|
|
||||||
export const registerUserGet = (api: typeof userApi) => {
|
export const registerUserGet = (api: typeof userApi) => {
|
||||||
return api.openapi(route, async (c) => {
|
return api.openapi(route, async (c) => {
|
||||||
const user = c.get('user')
|
const userId = await verifyAuthentication(c)
|
||||||
const result = await db.query.user.findFirst({
|
const result = await db.query.user.findFirst({
|
||||||
where: eq(userDb.id, user.id),
|
where: eq(userDb.id, userId),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
@ -20,10 +20,7 @@ const route = createRoute({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
204: {
|
||||||
content: {
|
|
||||||
'application/json': { schema: UserOutput },
|
|
||||||
},
|
|
||||||
description: 'Return success',
|
description: 'Return success',
|
||||||
},
|
},
|
||||||
...openApiErrorResponses,
|
...openApiErrorResponses,
|
||||||
@ -70,7 +67,11 @@ export const registerUserWebhook = (api: typeof userApi) => {
|
|||||||
case 'user.created': {
|
case 'user.created': {
|
||||||
const result = await userCreate({ payload: verifiedPayload })
|
const result = await userCreate({ payload: verifiedPayload })
|
||||||
logger.info('Clerk Webhook', result)
|
logger.info('Clerk Webhook', result)
|
||||||
return c.json(UserOutput.parse(result), 200)
|
if (result) {
|
||||||
|
return c.json({}, 204)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({}, 404)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
throw new HTTPException(404, { message: 'Webhook type not supported' })
|
throw new HTTPException(404, { message: 'Webhook type not supported' })
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { access_token, db, user } from '@boring.tools/database'
|
import { access_token, db, user } from '@boring.tools/database'
|
||||||
|
import { logger } from '@boring.tools/logger'
|
||||||
import { clerkMiddleware, getAuth } from '@hono/clerk-auth'
|
import { clerkMiddleware, getAuth } from '@hono/clerk-auth'
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import type { Context, Next } from 'hono'
|
import type { Context, Next } from 'hono'
|
||||||
@ -52,8 +53,9 @@ export const verifyAuthentication = async (c: Context) => {
|
|||||||
.where(eq(user.providerId, auth.userId))
|
.where(eq(user.providerId, auth.userId))
|
||||||
|
|
||||||
if (!userEntry) {
|
if (!userEntry) {
|
||||||
|
logger.error('User not found - Unauthorized', { providerId: auth.userId })
|
||||||
throw new HTTPException(401, { message: 'Unauthorized' })
|
throw new HTTPException(401, { message: 'Unauthorized' })
|
||||||
}
|
}
|
||||||
// console.log(userEntry)
|
|
||||||
return userEntry.id
|
return userEntry.id
|
||||||
}
|
}
|
||||||
|
@ -106,7 +106,6 @@ export function handleZodError(
|
|||||||
},
|
},
|
||||||
c: Context,
|
c: Context,
|
||||||
) {
|
) {
|
||||||
console.log('asdasasdas')
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
const error = SchemaError.fromZod(result.error, c)
|
const error = SchemaError.fromZod(result.error, c)
|
||||||
return c.json<z.infer<ReturnType<typeof createErrorSchema>>>(
|
return c.json<z.infer<ReturnType<typeof createErrorSchema>>>(
|
||||||
|
61
apps/api/src/utils/logger.ts
Normal file
61
apps/api/src/utils/logger.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { db, user } from '@boring.tools/database'
|
||||||
|
import { logger as log } from '@boring.tools/logger'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import type { MiddlewareHandler } from 'hono'
|
||||||
|
import { getPath } from 'hono/utils/url'
|
||||||
|
|
||||||
|
export const logger = (): MiddlewareHandler => {
|
||||||
|
return async function logga(c, next) {
|
||||||
|
const { method } = c.req
|
||||||
|
const clerkUser = c.get('clerkAuth')
|
||||||
|
const requestId = c.get('requestId')
|
||||||
|
const [dbUser] = await db
|
||||||
|
.select({ id: user.id, providerId: user.providerId })
|
||||||
|
.from(user)
|
||||||
|
.where(eq(user.providerId, clerkUser?.userId))
|
||||||
|
const path = getPath(c.req.raw)
|
||||||
|
|
||||||
|
log.info('Incoming', {
|
||||||
|
direction: 'in',
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
userId: dbUser?.id,
|
||||||
|
requestId,
|
||||||
|
})
|
||||||
|
|
||||||
|
await next()
|
||||||
|
|
||||||
|
if (c.res.status <= 399) {
|
||||||
|
log.info('Outgoing', {
|
||||||
|
direction: 'out',
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
status: c.res.status,
|
||||||
|
userId: dbUser?.id,
|
||||||
|
requestId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.res.status >= 400 && c.res.status < 499) {
|
||||||
|
log.warn('Outgoing', {
|
||||||
|
direction: 'out',
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
status: c.res.status,
|
||||||
|
userId: dbUser?.id,
|
||||||
|
requestId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.res.status >= 500) {
|
||||||
|
log.error('Outgoing', {
|
||||||
|
direction: 'out',
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
status: c.res.status,
|
||||||
|
userId: dbUser?.id,
|
||||||
|
requestId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -8,7 +8,11 @@ import {
|
|||||||
Separator,
|
Separator,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from '@boring.tools/ui'
|
} from '@boring.tools/ui'
|
||||||
|
import { useAuth } from '@clerk/clerk-react'
|
||||||
import { Link } from '@tanstack/react-router'
|
import { Link } from '@tanstack/react-router'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
import { useUser } from '../hooks/useUser'
|
||||||
|
|
||||||
type Breadcrumbs = {
|
type Breadcrumbs = {
|
||||||
name: string
|
name: string
|
||||||
@ -19,6 +23,15 @@ export const PageWrapper = ({
|
|||||||
children,
|
children,
|
||||||
breadcrumbs,
|
breadcrumbs,
|
||||||
}: { children: React.ReactNode; breadcrumbs?: Breadcrumbs[] }) => {
|
}: { children: React.ReactNode; breadcrumbs?: Breadcrumbs[] }) => {
|
||||||
|
const { error } = useUser()
|
||||||
|
const { signOut } = useAuth()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (error) {
|
||||||
|
signOut()
|
||||||
|
}
|
||||||
|
}, [error, signOut])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<header className="flex h-16 shrink-0 items-center gap-2">
|
<header className="flex h-16 shrink-0 items-center gap-2">
|
||||||
|
21
apps/app/src/hooks/useUser.ts
Normal file
21
apps/app/src/hooks/useUser.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import type { UserOutput } from '@boring.tools/schema'
|
||||||
|
import { useAuth } from '@clerk/clerk-react'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import type { z } from 'zod'
|
||||||
|
import { queryFetch } from '../utils/queryFetch'
|
||||||
|
|
||||||
|
type User = z.infer<typeof UserOutput>
|
||||||
|
|
||||||
|
export const useUser = () => {
|
||||||
|
const { getToken } = useAuth()
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['user'],
|
||||||
|
queryFn: async (): Promise<Readonly<User>> =>
|
||||||
|
await queryFetch({
|
||||||
|
path: 'user',
|
||||||
|
method: 'get',
|
||||||
|
token: await getToken(),
|
||||||
|
}),
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
}
|
@ -1,12 +1,12 @@
|
|||||||
/* prettier-ignore-start */
|
|
||||||
|
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
|
||||||
// This file is auto-generated by TanStack Router
|
// This file was automatically generated by TanStack Router.
|
||||||
|
// You should NOT make any changes in this file as it will be overwritten.
|
||||||
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||||
|
|
||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
@ -39,26 +39,31 @@ const ChangelogIdEditLazyImport = createFileRoute('/changelog/$id/edit')()
|
|||||||
// Create/Update Routes
|
// Create/Update Routes
|
||||||
|
|
||||||
const CliLazyRoute = CliLazyImport.update({
|
const CliLazyRoute = CliLazyImport.update({
|
||||||
|
id: '/cli',
|
||||||
path: '/cli',
|
path: '/cli',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/cli.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/cli.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const IndexLazyRoute = IndexLazyImport.update({
|
const IndexLazyRoute = IndexLazyImport.update({
|
||||||
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const UserIndexLazyRoute = UserIndexLazyImport.update({
|
const UserIndexLazyRoute = UserIndexLazyImport.update({
|
||||||
|
id: '/user/',
|
||||||
path: '/user/',
|
path: '/user/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/user/index.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/user/index.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const PageIndexLazyRoute = PageIndexLazyImport.update({
|
const PageIndexLazyRoute = PageIndexLazyImport.update({
|
||||||
|
id: '/page/',
|
||||||
path: '/page/',
|
path: '/page/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/page.index.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/page.index.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
||||||
|
id: '/changelog/',
|
||||||
path: '/changelog/',
|
path: '/changelog/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -66,6 +71,7 @@ const ChangelogIndexLazyRoute = ChangelogIndexLazyImport.update({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const AccessTokensIndexLazyRoute = AccessTokensIndexLazyImport.update({
|
const AccessTokensIndexLazyRoute = AccessTokensIndexLazyImport.update({
|
||||||
|
id: '/access-tokens/',
|
||||||
path: '/access-tokens/',
|
path: '/access-tokens/',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -73,16 +79,19 @@ const AccessTokensIndexLazyRoute = AccessTokensIndexLazyImport.update({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const PageCreateLazyRoute = PageCreateLazyImport.update({
|
const PageCreateLazyRoute = PageCreateLazyImport.update({
|
||||||
|
id: '/page/create',
|
||||||
path: '/page/create',
|
path: '/page/create',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/page.create.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/page.create.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const PageIdLazyRoute = PageIdLazyImport.update({
|
const PageIdLazyRoute = PageIdLazyImport.update({
|
||||||
|
id: '/page/$id',
|
||||||
path: '/page/$id',
|
path: '/page/$id',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/page.$id.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/page.$id.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const ChangelogCreateLazyRoute = ChangelogCreateLazyImport.update({
|
const ChangelogCreateLazyRoute = ChangelogCreateLazyImport.update({
|
||||||
|
id: '/changelog/create',
|
||||||
path: '/changelog/create',
|
path: '/changelog/create',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -90,11 +99,13 @@ const ChangelogCreateLazyRoute = ChangelogCreateLazyImport.update({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const ChangelogIdLazyRoute = ChangelogIdLazyImport.update({
|
const ChangelogIdLazyRoute = ChangelogIdLazyImport.update({
|
||||||
|
id: '/changelog/$id',
|
||||||
path: '/changelog/$id',
|
path: '/changelog/$id',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() => import('./routes/changelog.$id.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/changelog.$id.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const AccessTokensNewLazyRoute = AccessTokensNewLazyImport.update({
|
const AccessTokensNewLazyRoute = AccessTokensNewLazyImport.update({
|
||||||
|
id: '/access-tokens/new',
|
||||||
path: '/access-tokens/new',
|
path: '/access-tokens/new',
|
||||||
getParentRoute: () => rootRoute,
|
getParentRoute: () => rootRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -102,6 +113,7 @@ const AccessTokensNewLazyRoute = AccessTokensNewLazyImport.update({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const PageIdIndexLazyRoute = PageIdIndexLazyImport.update({
|
const PageIdIndexLazyRoute = PageIdIndexLazyImport.update({
|
||||||
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => PageIdLazyRoute,
|
getParentRoute: () => PageIdLazyRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -109,6 +121,7 @@ const PageIdIndexLazyRoute = PageIdIndexLazyImport.update({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const ChangelogIdIndexLazyRoute = ChangelogIdIndexLazyImport.update({
|
const ChangelogIdIndexLazyRoute = ChangelogIdIndexLazyImport.update({
|
||||||
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => ChangelogIdLazyRoute,
|
getParentRoute: () => ChangelogIdLazyRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -116,12 +129,14 @@ const ChangelogIdIndexLazyRoute = ChangelogIdIndexLazyImport.update({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const PageIdEditLazyRoute = PageIdEditLazyImport.update({
|
const PageIdEditLazyRoute = PageIdEditLazyImport.update({
|
||||||
|
id: '/edit',
|
||||||
path: '/edit',
|
path: '/edit',
|
||||||
getParentRoute: () => PageIdLazyRoute,
|
getParentRoute: () => PageIdLazyRoute,
|
||||||
} as any).lazy(() => import('./routes/page.$id.edit.lazy').then((d) => d.Route))
|
} as any).lazy(() => import('./routes/page.$id.edit.lazy').then((d) => d.Route))
|
||||||
|
|
||||||
const ChangelogIdVersionCreateLazyRoute =
|
const ChangelogIdVersionCreateLazyRoute =
|
||||||
ChangelogIdVersionCreateLazyImport.update({
|
ChangelogIdVersionCreateLazyImport.update({
|
||||||
|
id: '/versionCreate',
|
||||||
path: '/versionCreate',
|
path: '/versionCreate',
|
||||||
getParentRoute: () => ChangelogIdLazyRoute,
|
getParentRoute: () => ChangelogIdLazyRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -129,6 +144,7 @@ const ChangelogIdVersionCreateLazyRoute =
|
|||||||
)
|
)
|
||||||
|
|
||||||
const ChangelogIdEditLazyRoute = ChangelogIdEditLazyImport.update({
|
const ChangelogIdEditLazyRoute = ChangelogIdEditLazyImport.update({
|
||||||
|
id: '/edit',
|
||||||
path: '/edit',
|
path: '/edit',
|
||||||
getParentRoute: () => ChangelogIdLazyRoute,
|
getParentRoute: () => ChangelogIdLazyRoute,
|
||||||
} as any).lazy(() =>
|
} as any).lazy(() =>
|
||||||
@ -137,6 +153,7 @@ const ChangelogIdEditLazyRoute = ChangelogIdEditLazyImport.update({
|
|||||||
|
|
||||||
const ChangelogIdVersionVersionIdRoute =
|
const ChangelogIdVersionVersionIdRoute =
|
||||||
ChangelogIdVersionVersionIdImport.update({
|
ChangelogIdVersionVersionIdImport.update({
|
||||||
|
id: '/version/$versionId',
|
||||||
path: '/version/$versionId',
|
path: '/version/$versionId',
|
||||||
getParentRoute: () => ChangelogIdLazyRoute,
|
getParentRoute: () => ChangelogIdLazyRoute,
|
||||||
} as any)
|
} as any)
|
||||||
@ -451,8 +468,6 @@ export const routeTree = rootRoute
|
|||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
._addFileTypes<FileRouteTypes>()
|
._addFileTypes<FileRouteTypes>()
|
||||||
|
|
||||||
/* prettier-ignore-end */
|
|
||||||
|
|
||||||
/* ROUTE_MANIFEST_START
|
/* ROUTE_MANIFEST_START
|
||||||
{
|
{
|
||||||
"routes": {
|
"routes": {
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -58,66 +62,118 @@ const Component = () => {
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="space-y-8 max-w-screen-md"
|
className="flex flex-col gap-10 w-full max-w-screen-lg"
|
||||||
>
|
>
|
||||||
<FormField
|
<div className="flex gap-10 w-full max-w-screen-lg">
|
||||||
control={form.control}
|
<Card className="w-full">
|
||||||
name="title"
|
<CardHeader>
|
||||||
render={({ field }) => (
|
<CardTitle>Details</CardTitle>
|
||||||
<FormItem>
|
</CardHeader>
|
||||||
<FormLabel>Title</FormLabel>
|
<CardContent>
|
||||||
<FormControl>
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<Input placeholder="My changelog" {...field} autoFocus />
|
<FormField
|
||||||
</FormControl>{' '}
|
control={form.control}
|
||||||
<FormMessage />
|
name="title"
|
||||||
</FormItem>
|
render={({ field }) => (
|
||||||
)}
|
<FormItem>
|
||||||
/>
|
<FormLabel>Title</FormLabel>
|
||||||
|
<FormControl>
|
||||||
<FormField
|
<Input
|
||||||
control={form.control}
|
placeholder="My changelog"
|
||||||
name="description"
|
{...field}
|
||||||
render={({ field }) => (
|
autoFocus
|
||||||
<FormItem>
|
/>
|
||||||
<FormLabel>Description</FormLabel>
|
</FormControl>{' '}
|
||||||
<FormControl>
|
<FormMessage />
|
||||||
<Textarea
|
</FormItem>
|
||||||
placeholder="Some details about the changelog..."
|
)}
|
||||||
{...field}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>{' '}
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="isSemver"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>Description</FormLabel>
|
||||||
<Checkbox
|
<FormControl>
|
||||||
checked={field.value}
|
<Textarea
|
||||||
onCheckedChange={field.onChange}
|
placeholder="Some details about the changelog..."
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>{' '}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
|
||||||
<div className="space-y-1 leading-none">
|
|
||||||
<FormLabel>Using Semver</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
If this changelog is following the{' '}
|
|
||||||
<a
|
|
||||||
href="https://semver.org/lang/de/"
|
|
||||||
className="text-emerald-700"
|
|
||||||
>
|
|
||||||
semantic versioning?
|
|
||||||
</a>
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</CardContent>
|
||||||
)}
|
</Card>
|
||||||
/>
|
|
||||||
<div className="flex gap-5">
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Options</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="w-full flex flex-col gap-5">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isSemver"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>Using Semver</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
If this changelog is following the{' '}
|
||||||
|
<a
|
||||||
|
href="https://semver.org/lang/de/"
|
||||||
|
className="text-emerald-700"
|
||||||
|
>
|
||||||
|
semantic versioning?
|
||||||
|
</a>
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isConventional"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>Using Conventional Commits</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
If this changelog is using{' '}
|
||||||
|
<a
|
||||||
|
href="https://www.conventionalcommits.org/en/v1.0.0/"
|
||||||
|
className="text-emerald-700"
|
||||||
|
>
|
||||||
|
conventional commits
|
||||||
|
</a>
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-end justify-end gap-5">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
@ -127,7 +183,8 @@ const Component = () => {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit">Update</Button>
|
|
||||||
|
<Button type="submit">Save</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { ChangelogCreateInput } from '@boring.tools/schema'
|
import { ChangelogCreateInput } from '@boring.tools/schema'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -53,97 +57,131 @@ const Component = () => {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<h1 className="text-3xl">New changelog</h1>
|
<h1 className="text-3xl">New changelog</h1>
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="space-y-8 max-w-screen-md"
|
className="flex flex-col gap-10 w-full max-w-screen-lg"
|
||||||
>
|
>
|
||||||
<FormField
|
<div className="flex gap-10 w-full max-w-screen-lg">
|
||||||
control={form.control}
|
<Card className="w-full">
|
||||||
name="title"
|
<CardHeader>
|
||||||
render={({ field }) => (
|
<CardTitle>Details</CardTitle>
|
||||||
<FormItem>
|
</CardHeader>
|
||||||
<FormLabel>Title</FormLabel>
|
<CardContent>
|
||||||
<FormControl>
|
<div className="flex flex-col gap-3 w-full">
|
||||||
<Input placeholder="My changelog" {...field} autoFocus />
|
<FormField
|
||||||
</FormControl>{' '}
|
control={form.control}
|
||||||
<FormMessage />
|
name="title"
|
||||||
</FormItem>
|
render={({ field }) => (
|
||||||
)}
|
<FormItem>
|
||||||
/>
|
<FormLabel>Title</FormLabel>
|
||||||
|
<FormControl>
|
||||||
<FormField
|
<Input
|
||||||
control={form.control}
|
placeholder="My changelog"
|
||||||
name="description"
|
{...field}
|
||||||
render={({ field }) => (
|
autoFocus
|
||||||
<FormItem>
|
/>
|
||||||
<FormLabel>Description</FormLabel>
|
</FormControl>{' '}
|
||||||
<FormControl>
|
<FormMessage />
|
||||||
<Textarea
|
</FormItem>
|
||||||
placeholder="Some details about the changelog..."
|
)}
|
||||||
{...field}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>{' '}
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="isSemver"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>Description</FormLabel>
|
||||||
<Checkbox
|
<FormControl>
|
||||||
checked={field.value}
|
<Textarea
|
||||||
onCheckedChange={field.onChange}
|
placeholder="Some details about the changelog..."
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>{' '}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
|
||||||
<div className="space-y-1 leading-none">
|
|
||||||
<FormLabel>Using Semver</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
If this changelog is following the{' '}
|
|
||||||
<a
|
|
||||||
href="https://semver.org/lang/de/"
|
|
||||||
className="text-emerald-700"
|
|
||||||
>
|
|
||||||
semantic versioning?
|
|
||||||
</a>
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</CardContent>
|
||||||
)}
|
</Card>
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<Card className="w-full">
|
||||||
control={form.control}
|
<CardHeader>
|
||||||
name="isConventional"
|
<CardTitle>Options</CardTitle>
|
||||||
render={({ field }) => (
|
</CardHeader>
|
||||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
<CardContent>
|
||||||
<FormControl>
|
<div className="w-full flex flex-col gap-5">
|
||||||
<Checkbox
|
<FormField
|
||||||
checked={field.value}
|
control={form.control}
|
||||||
onCheckedChange={field.onChange}
|
name="isSemver"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>Using Semver</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
If this changelog is following the{' '}
|
||||||
|
<a
|
||||||
|
href="https://semver.org/lang/de/"
|
||||||
|
className="text-emerald-700"
|
||||||
|
>
|
||||||
|
semantic versioning?
|
||||||
|
</a>
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="isConventional"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md ">
|
||||||
|
<FormControl>
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<div className="space-y-1 leading-none">
|
||||||
|
<FormLabel>Using Conventional Commits</FormLabel>
|
||||||
|
<FormDescription>
|
||||||
|
If this changelog is using{' '}
|
||||||
|
<a
|
||||||
|
href="https://www.conventionalcommits.org/en/v1.0.0/"
|
||||||
|
className="text-emerald-700"
|
||||||
|
>
|
||||||
|
conventional commits
|
||||||
|
</a>
|
||||||
|
</FormDescription>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
|
||||||
<div className="space-y-1 leading-none">
|
|
||||||
<FormLabel>Using Conventional Commits</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
If this changelog is using{' '}
|
|
||||||
<a
|
|
||||||
href="https://www.conventionalcommits.org/en/v1.0.0/"
|
|
||||||
className="text-emerald-700"
|
|
||||||
>
|
|
||||||
conventional commits
|
|
||||||
</a>
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
</div>
|
||||||
</FormItem>
|
</CardContent>
|
||||||
)}
|
</Card>
|
||||||
/>
|
</div>
|
||||||
<Button type="submit">Create</Button>
|
|
||||||
|
<div className="flex items-end justify-end gap-5">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={'ghost'}
|
||||||
|
onClick={() => navigate({ to: '/changelog' })}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit">Create</Button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { PageUpdateInput } from '@boring.tools/schema'
|
import { PageUpdateInput } from '@boring.tools/schema'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
@ -57,129 +61,147 @@ const Component = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<h1 className="text-3xl">Edit page</h1>
|
|
||||||
|
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="space-y-8 max-w-screen-md"
|
className="flex flex-col gap-10 w-full max-w-screen-lg"
|
||||||
>
|
>
|
||||||
<FormField
|
<div className="flex gap-10 w-full">
|
||||||
control={form.control}
|
<Card className="w-full">
|
||||||
name="title"
|
<CardHeader>
|
||||||
render={({ field }) => (
|
<CardTitle>Details</CardTitle>
|
||||||
<FormItem>
|
</CardHeader>
|
||||||
<FormLabel>Title</FormLabel>
|
<CardContent className="flex flex-col gap-5">
|
||||||
<FormControl>
|
<FormField
|
||||||
<Input placeholder="My page" {...field} autoFocus />
|
control={form.control}
|
||||||
</FormControl>{' '}
|
name="title"
|
||||||
<FormMessage />
|
render={({ field }) => (
|
||||||
</FormItem>
|
<FormItem>
|
||||||
)}
|
<FormLabel>Title</FormLabel>
|
||||||
/>
|
<FormControl>
|
||||||
|
<Input placeholder="My page" {...field} autoFocus />
|
||||||
|
</FormControl>{' '}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="description"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Description</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="Some details about the page..."
|
placeholder="Some details about the page..."
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
value={field.value ?? ''}
|
||||||
</FormControl>{' '}
|
/>
|
||||||
<FormMessage />
|
</FormControl>{' '}
|
||||||
</FormItem>
|
<FormMessage />
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<FormField
|
<Card className="w-full">
|
||||||
control={form.control}
|
<CardHeader>
|
||||||
name="changelogIds"
|
<CardTitle>Options</CardTitle>
|
||||||
render={({ field }) => (
|
</CardHeader>
|
||||||
<FormItem className="flex flex-col">
|
<CardContent className="flex flex-col gap-5">
|
||||||
<FormLabel>Changelogs</FormLabel>
|
<FormField
|
||||||
<Popover>
|
control={form.control}
|
||||||
<PopoverTrigger asChild>
|
name="changelogIds"
|
||||||
<FormControl>
|
render={({ field }) => (
|
||||||
<Button
|
<FormItem className="flex flex-col">
|
||||||
variant="outline"
|
<FormLabel>Changelogs</FormLabel>
|
||||||
role="combobox"
|
<Popover>
|
||||||
className={cn(
|
<PopoverTrigger asChild>
|
||||||
'w-[200px] justify-between',
|
<FormControl>
|
||||||
!field.value && 'text-muted-foreground',
|
<Button
|
||||||
)}
|
variant="outline"
|
||||||
>
|
role="combobox"
|
||||||
{field?.value?.length === 1 &&
|
className={cn(
|
||||||
changelogList.data?.find((changelog) =>
|
'w-[200px] justify-between',
|
||||||
field.value?.includes(changelog.id),
|
!field.value && 'text-muted-foreground',
|
||||||
)?.title}
|
)}
|
||||||
{field?.value &&
|
|
||||||
field.value.length <= 0 &&
|
|
||||||
'No changelog selected'}
|
|
||||||
{field?.value &&
|
|
||||||
field.value.length > 1 &&
|
|
||||||
`${field?.value?.length} selected`}
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</FormControl>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-[200px] p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder="Search changelogs..." />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>No changelog found.</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{changelogList.data?.map((changelog) => (
|
|
||||||
<CommandItem
|
|
||||||
value={changelog.title}
|
|
||||||
key={changelog.id}
|
|
||||||
onSelect={() => {
|
|
||||||
const getIds = () => {
|
|
||||||
if (!field.value) {
|
|
||||||
return [changelog.id]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.value?.includes(changelog.id)) {
|
|
||||||
return field.value.filter(
|
|
||||||
(id) => id !== changelog.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
...(field?.value as string[]),
|
|
||||||
changelog.id,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
form.setValue('changelogIds', getIds())
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Check
|
{field?.value?.length === 1 &&
|
||||||
className={cn(
|
changelogList.data?.find((changelog) =>
|
||||||
'mr-2 h-4 w-4',
|
field.value?.includes(changelog.id),
|
||||||
field.value?.includes(changelog.id)
|
)?.title}
|
||||||
? 'opacity-100'
|
{field?.value &&
|
||||||
: 'opacity-0',
|
field.value.length <= 0 &&
|
||||||
)}
|
'No changelog selected'}
|
||||||
/>
|
{field?.value &&
|
||||||
{changelog.title}
|
field.value.length > 1 &&
|
||||||
</CommandItem>
|
`${field?.value?.length} selected`}
|
||||||
))}
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</CommandGroup>
|
</Button>
|
||||||
</CommandList>
|
</FormControl>
|
||||||
</Command>
|
</PopoverTrigger>
|
||||||
</PopoverContent>
|
<PopoverContent className="w-[200px] p-0">
|
||||||
</Popover>
|
<Command>
|
||||||
<FormDescription>
|
<CommandInput placeholder="Search changelogs..." />
|
||||||
This changelogs are shown on this page.
|
<CommandList>
|
||||||
</FormDescription>
|
<CommandEmpty>No changelog found.</CommandEmpty>
|
||||||
<FormMessage />
|
<CommandGroup>
|
||||||
</FormItem>
|
{changelogList.data?.map((changelog) => (
|
||||||
)}
|
<CommandItem
|
||||||
/>
|
value={changelog.title}
|
||||||
<div className="flex gap-5">
|
key={changelog.id}
|
||||||
|
onSelect={() => {
|
||||||
|
const getIds = () => {
|
||||||
|
if (!field.value) {
|
||||||
|
return [changelog.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
field.value?.includes(changelog.id)
|
||||||
|
) {
|
||||||
|
return field.value.filter(
|
||||||
|
(id) => id !== changelog.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...(field?.value as string[]),
|
||||||
|
changelog.id,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
form.setValue('changelogIds', getIds())
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
'mr-2 h-4 w-4',
|
||||||
|
field.value?.includes(changelog.id)
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-0',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{changelog.title}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormDescription>
|
||||||
|
This changelogs are shown on this page.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-end justify-end gap-5">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant={'ghost'}
|
variant={'ghost'}
|
||||||
@ -187,7 +209,7 @@ const Component = () => {
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit">Update</Button>
|
<Button type="submit">Save</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { PageCreateInput } from '@boring.tools/schema'
|
import { PageCreateInput } from '@boring.tools/schema'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
CommandGroup,
|
CommandGroup,
|
||||||
@ -69,115 +73,146 @@ const Component = () => {
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="space-y-8 max-w-screen-md"
|
className="flex flex-col gap-10 w-full max-w-screen-lg"
|
||||||
>
|
>
|
||||||
<FormField
|
<div className="flex gap-10 w-full">
|
||||||
control={form.control}
|
<Card className="w-full">
|
||||||
name="title"
|
<CardHeader>
|
||||||
render={({ field }) => (
|
<CardTitle>Details</CardTitle>
|
||||||
<FormItem>
|
</CardHeader>
|
||||||
<FormLabel>Title</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="My page" {...field} autoFocus />
|
|
||||||
</FormControl>{' '}
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<CardContent className="flex flex-col gap-5">
|
||||||
control={form.control}
|
<FormField
|
||||||
name="description"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="title"
|
||||||
<FormItem>
|
render={({ field }) => (
|
||||||
<FormLabel>Description</FormLabel>
|
<FormItem>
|
||||||
<FormControl>
|
<FormLabel>Title</FormLabel>
|
||||||
<Textarea
|
<FormControl>
|
||||||
placeholder="Some details about the page..."
|
<Input placeholder="My page" {...field} autoFocus />
|
||||||
{...field}
|
</FormControl>{' '}
|
||||||
/>
|
<FormMessage />
|
||||||
</FormControl>{' '}
|
</FormItem>
|
||||||
<FormMessage />
|
)}
|
||||||
</FormItem>
|
/>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="changelogIds"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="flex flex-col">
|
<FormItem>
|
||||||
<FormLabel>Changelogs</FormLabel>
|
<FormLabel>Description</FormLabel>
|
||||||
<Popover>
|
<FormControl>
|
||||||
<PopoverTrigger asChild>
|
<Textarea
|
||||||
<FormControl>
|
placeholder="Some details about the page..."
|
||||||
<Button
|
{...field}
|
||||||
variant="outline"
|
value={field.value ?? ''}
|
||||||
role="combobox"
|
/>
|
||||||
className={cn(
|
</FormControl>{' '}
|
||||||
'w-[200px] justify-between',
|
<FormMessage />
|
||||||
!field.value && 'text-muted-foreground',
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
>
|
/>
|
||||||
{field.value.length === 1 &&
|
</CardContent>
|
||||||
changelogList.data?.find((changelog) =>
|
</Card>
|
||||||
field.value?.includes(changelog.id),
|
|
||||||
)?.title}
|
|
||||||
{field.value.length <= 0 && 'No changelog selected'}
|
|
||||||
{field.value.length > 1 &&
|
|
||||||
`${field.value.length} selected`}
|
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</FormControl>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-[200px] p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder="Search changelogs..." />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>No changelog found.</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{changelogList.data?.map((changelog) => (
|
|
||||||
<CommandItem
|
|
||||||
value={changelog.title}
|
|
||||||
key={changelog.id}
|
|
||||||
onSelect={() => {
|
|
||||||
const getIds = () => {
|
|
||||||
if (field.value.includes(changelog.id)) {
|
|
||||||
const asd = field.value.filter(
|
|
||||||
(id) => id !== changelog.id,
|
|
||||||
)
|
|
||||||
return asd
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...field.value, changelog.id]
|
<Card className="w-full">
|
||||||
}
|
<CardHeader>
|
||||||
form.setValue('changelogIds', getIds())
|
<CardTitle>Options</CardTitle>
|
||||||
}}
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-5">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="changelogIds"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col">
|
||||||
|
<FormLabel>Changelogs</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
'w-[200px] justify-between',
|
||||||
|
!field.value && 'text-muted-foreground',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Check
|
{field.value.length === 1 &&
|
||||||
className={cn(
|
changelogList.data?.find((changelog) =>
|
||||||
'mr-2 h-4 w-4',
|
field.value?.includes(changelog.id),
|
||||||
field.value.includes(changelog.id)
|
)?.title}
|
||||||
? 'opacity-100'
|
{field.value.length <= 0 &&
|
||||||
: 'opacity-0',
|
'No changelog selected'}
|
||||||
)}
|
{field.value.length > 1 &&
|
||||||
/>
|
`${field.value.length} selected`}
|
||||||
{changelog.title}
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
</CommandItem>
|
</Button>
|
||||||
))}
|
</FormControl>
|
||||||
</CommandGroup>
|
</PopoverTrigger>
|
||||||
</CommandList>
|
<PopoverContent className="w-[200px] p-0">
|
||||||
</Command>
|
<Command>
|
||||||
</PopoverContent>
|
<CommandInput placeholder="Search changelogs..." />
|
||||||
</Popover>
|
<CommandList>
|
||||||
<FormDescription>
|
<CommandEmpty>No changelog found.</CommandEmpty>
|
||||||
This changelogs are shown on this page.
|
<CommandGroup>
|
||||||
</FormDescription>
|
{changelogList.data?.map((changelog) => (
|
||||||
<FormMessage />
|
<CommandItem
|
||||||
</FormItem>
|
value={changelog.title}
|
||||||
)}
|
key={changelog.id}
|
||||||
/>
|
onSelect={() => {
|
||||||
<Button type="submit">Create</Button>
|
const getIds = () => {
|
||||||
|
if (
|
||||||
|
field.value.includes(changelog.id)
|
||||||
|
) {
|
||||||
|
const asd = field.value.filter(
|
||||||
|
(id) => id !== changelog.id,
|
||||||
|
)
|
||||||
|
return asd
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...field.value, changelog.id]
|
||||||
|
}
|
||||||
|
form.setValue('changelogIds', getIds())
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
'mr-2 h-4 w-4',
|
||||||
|
field.value.includes(changelog.id)
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-0',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{changelog.title}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormDescription>
|
||||||
|
This changelogs are shown on this page.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end justify-end gap-5">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={'ghost'}
|
||||||
|
onClick={() => navigate({ to: '/page' })}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit">Create</Button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,13 +12,17 @@ const url = import.meta.env.PROD
|
|||||||
: 'http://localhost:3000'
|
: 'http://localhost:3000'
|
||||||
|
|
||||||
export const queryFetch = async ({ path, method, data, token }: Fetch) => {
|
export const queryFetch = async ({ path, method, data, token }: Fetch) => {
|
||||||
const response = await axios({
|
try {
|
||||||
method,
|
const response = await axios({
|
||||||
url: `${url}/v1/${path}`,
|
method,
|
||||||
data,
|
url: `${url}/v1/${path}`,
|
||||||
headers: {
|
data,
|
||||||
Authorization: `Bearer ${token}`,
|
headers: {
|
||||||
},
|
Authorization: `Bearer ${token}`,
|
||||||
})
|
},
|
||||||
return response.data
|
})
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Somethind went wrong.')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,6 @@
|
|||||||
"formatter": {
|
"formatter": {
|
||||||
"jsxQuoteStyle": "double",
|
"jsxQuoteStyle": "double",
|
||||||
"quoteProperties": "asNeeded",
|
"quoteProperties": "asNeeded",
|
||||||
"trailingCommas": "all",
|
|
||||||
"semicolons": "asNeeded",
|
"semicolons": "asNeeded",
|
||||||
"arrowParentheses": "always",
|
"arrowParentheses": "always",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
|
57
ci/development/docker-compose.yaml
Normal file
57
ci/development/docker-compose.yaml
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
container_name: boring_services_postgres
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=postgres
|
||||||
|
- POSTGRES_USER=postgres
|
||||||
|
- POSTGRES_DB=postgres
|
||||||
|
|
||||||
|
cache:
|
||||||
|
container_name: boring_services_redis
|
||||||
|
image: redis:7.4.1-alpine
|
||||||
|
ports:
|
||||||
|
- '6379:6379'
|
||||||
|
command: redis-server --save 20 1 --loglevel warning --requirepass development
|
||||||
|
|
||||||
|
loki:
|
||||||
|
image: grafana/loki:latest
|
||||||
|
container_name: boring_services_loki
|
||||||
|
ports:
|
||||||
|
- "9100:3100"
|
||||||
|
command: -config.file=/etc/loki/local-config.yaml
|
||||||
|
|
||||||
|
grafana:
|
||||||
|
image: grafana/grafana:latest
|
||||||
|
container_name: boring_services_grafana
|
||||||
|
environment:
|
||||||
|
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
|
||||||
|
- GF_AUTH_ANONYMOUS_ENABLED=true
|
||||||
|
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
|
||||||
|
volumes:
|
||||||
|
- ./grafana/dashboard.yaml:/etc/grafana/provisioning/dashboards/main.yaml
|
||||||
|
- ./grafana/dashboards:/var/lib/grafana/dashboards
|
||||||
|
- ./grafana/datasources:/etc/grafana/provisioning/datasources
|
||||||
|
# entrypoint:
|
||||||
|
# - sh
|
||||||
|
# - -euc
|
||||||
|
# - |
|
||||||
|
# mkdir -p /etc/grafana/provisioning/datasources
|
||||||
|
# cat <<EOF > /etc/grafana/provisioning/datasources/ds.yaml
|
||||||
|
# apiVersion: 1
|
||||||
|
# datasources:
|
||||||
|
# - name: Loki
|
||||||
|
# type: loki
|
||||||
|
# access: proxy
|
||||||
|
# orgId: 1
|
||||||
|
# url: http://loki:3100
|
||||||
|
# basicAuth: false
|
||||||
|
# isDefault: true
|
||||||
|
# version: 1
|
||||||
|
# editable: false
|
||||||
|
# EOF
|
||||||
|
# /run.sh
|
||||||
|
ports:
|
||||||
|
- "9000:3000"
|
12
ci/development/grafana/dashboard.yaml
Normal file
12
ci/development/grafana/dashboard.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
providers:
|
||||||
|
- name: "Dashboard provider"
|
||||||
|
orgId: 1
|
||||||
|
type: file
|
||||||
|
disableDeletion: false
|
||||||
|
updateIntervalSeconds: 10
|
||||||
|
allowUiUpdates: false
|
||||||
|
options:
|
||||||
|
path: /var/lib/grafana/dashboards
|
||||||
|
foldersFromFilesStructure: true
|
546
ci/development/grafana/dashboards/api-logs.json
Normal file
546
ci/development/grafana/dashboards/api-logs.json
Normal file
@ -0,0 +1,546 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": 1,
|
||||||
|
"links": [],
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"collapsed": false,
|
||||||
|
"gridPos": {
|
||||||
|
"h": 1,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 6,
|
||||||
|
"panels": [],
|
||||||
|
"title": "Service: API",
|
||||||
|
"type": "row"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "P8E80F9AEF21F6940"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "palette-classic"
|
||||||
|
},
|
||||||
|
"custom": {
|
||||||
|
"axisBorderShow": false,
|
||||||
|
"axisCenteredZero": false,
|
||||||
|
"axisColorMode": "text",
|
||||||
|
"axisLabel": "",
|
||||||
|
"axisPlacement": "auto",
|
||||||
|
"barAlignment": 0,
|
||||||
|
"barWidthFactor": 0.6,
|
||||||
|
"drawStyle": "points",
|
||||||
|
"fillOpacity": 0,
|
||||||
|
"gradientMode": "none",
|
||||||
|
"hideFrom": {
|
||||||
|
"legend": false,
|
||||||
|
"tooltip": false,
|
||||||
|
"viz": false
|
||||||
|
},
|
||||||
|
"insertNulls": false,
|
||||||
|
"lineInterpolation": "smooth",
|
||||||
|
"lineWidth": 1,
|
||||||
|
"pointSize": 5,
|
||||||
|
"scaleDistribution": {
|
||||||
|
"type": "linear"
|
||||||
|
},
|
||||||
|
"showPoints": "auto",
|
||||||
|
"spanNulls": 5000,
|
||||||
|
"stacking": {
|
||||||
|
"group": "A",
|
||||||
|
"mode": "none"
|
||||||
|
},
|
||||||
|
"thresholdsStyle": {
|
||||||
|
"mode": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "#73BF69",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 7,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 1
|
||||||
|
},
|
||||||
|
"id": 11,
|
||||||
|
"options": {
|
||||||
|
"legend": {
|
||||||
|
"calcs": ["sum"],
|
||||||
|
"displayMode": "list",
|
||||||
|
"placement": "bottom",
|
||||||
|
"showLegend": true
|
||||||
|
},
|
||||||
|
"tooltip": {
|
||||||
|
"mode": "single",
|
||||||
|
"sort": "none"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.3.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"editorMode": "builder",
|
||||||
|
"expr": "count(rate({service=\"api\"} | json | status <= 399 [$__auto]))",
|
||||||
|
"legendFormat": "<= 300",
|
||||||
|
"queryType": "range",
|
||||||
|
"refId": "A"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "P8E80F9AEF21F6940"
|
||||||
|
},
|
||||||
|
"editorMode": "code",
|
||||||
|
"expr": "count(rate({service=\"api\"} | json | status >= 400 and status <= 499 [$__auto]))",
|
||||||
|
"hide": false,
|
||||||
|
"legendFormat": "400 - 499",
|
||||||
|
"queryType": "range",
|
||||||
|
"refId": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "P8E80F9AEF21F6940"
|
||||||
|
},
|
||||||
|
"editorMode": "builder",
|
||||||
|
"expr": "count(rate({service=\"api\"} | json | status >= 500 [$__auto]))",
|
||||||
|
"hide": false,
|
||||||
|
"legendFormat": ">= 500",
|
||||||
|
"queryType": "range",
|
||||||
|
"refId": "C"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Requests",
|
||||||
|
"type": "timeseries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "P8E80F9AEF21F6940"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "green",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 4,
|
||||||
|
"x": 0,
|
||||||
|
"y": 8
|
||||||
|
},
|
||||||
|
"id": 10,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"percentChangeColorMode": "standard",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["sum"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showPercentChange": false,
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.3.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"editorMode": "builder",
|
||||||
|
"expr": "count(rate({service=\"api\"} | json | status <= 399 [$__auto]))",
|
||||||
|
"queryType": "range",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "<= 399",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "P8E80F9AEF21F6940"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "yellow",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 4,
|
||||||
|
"x": 4,
|
||||||
|
"y": 8
|
||||||
|
},
|
||||||
|
"id": 3,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"percentChangeColorMode": "standard",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["sum"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showPercentChange": false,
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.3.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"editorMode": "code",
|
||||||
|
"expr": "count(rate({service=\"api\"} | json | status >= 400 and status <= 499 [$__auto]))",
|
||||||
|
"queryType": "range",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "400 - 499",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "P8E80F9AEF21F6940"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 4,
|
||||||
|
"x": 8,
|
||||||
|
"y": 8
|
||||||
|
},
|
||||||
|
"id": 4,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"percentChangeColorMode": "standard",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["sum"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showPercentChange": false,
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.3.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"editorMode": "code",
|
||||||
|
"expr": "count(rate({service=\"api\"} | json | status >= 500 [$__auto]))",
|
||||||
|
"queryType": "range",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "400 - 499",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "P8E80F9AEF21F6940"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "blue",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 4,
|
||||||
|
"x": 0,
|
||||||
|
"y": 16
|
||||||
|
},
|
||||||
|
"id": 7,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"percentChangeColorMode": "standard",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["sum"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showPercentChange": false,
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.3.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"editorMode": "builder",
|
||||||
|
"expr": "count(rate({service=\"api\"} | json | level = `info` [$__auto]))",
|
||||||
|
"queryType": "range",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Info",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "P8E80F9AEF21F6940"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "yellow",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 4,
|
||||||
|
"x": 4,
|
||||||
|
"y": 16
|
||||||
|
},
|
||||||
|
"id": 8,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"percentChangeColorMode": "standard",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["sum"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showPercentChange": false,
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.3.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"editorMode": "builder",
|
||||||
|
"expr": "count(rate({service=\"api\"} | json | level = `warn` [$__auto]))",
|
||||||
|
"queryType": "range",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Warning",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "P8E80F9AEF21F6940"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {
|
||||||
|
"color": {
|
||||||
|
"mode": "thresholds"
|
||||||
|
},
|
||||||
|
"mappings": [],
|
||||||
|
"thresholds": {
|
||||||
|
"mode": "absolute",
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"color": "red",
|
||||||
|
"value": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 8,
|
||||||
|
"w": 4,
|
||||||
|
"x": 8,
|
||||||
|
"y": 16
|
||||||
|
},
|
||||||
|
"id": 9,
|
||||||
|
"options": {
|
||||||
|
"colorMode": "value",
|
||||||
|
"graphMode": "none",
|
||||||
|
"justifyMode": "auto",
|
||||||
|
"orientation": "auto",
|
||||||
|
"percentChangeColorMode": "standard",
|
||||||
|
"reduceOptions": {
|
||||||
|
"calcs": ["sum"],
|
||||||
|
"fields": "",
|
||||||
|
"values": false
|
||||||
|
},
|
||||||
|
"showPercentChange": false,
|
||||||
|
"textMode": "auto",
|
||||||
|
"wideLayout": true
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.3.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"editorMode": "builder",
|
||||||
|
"expr": "count(rate({service=\"api\"} | json | level = `error` [$__auto]))",
|
||||||
|
"queryType": "range",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Error",
|
||||||
|
"type": "stat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "loki",
|
||||||
|
"uid": "P8E80F9AEF21F6940"
|
||||||
|
},
|
||||||
|
"fieldConfig": {
|
||||||
|
"defaults": {},
|
||||||
|
"overrides": []
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 17,
|
||||||
|
"w": 24,
|
||||||
|
"x": 0,
|
||||||
|
"y": 24
|
||||||
|
},
|
||||||
|
"id": 5,
|
||||||
|
"options": {
|
||||||
|
"dedupStrategy": "none",
|
||||||
|
"enableLogDetails": true,
|
||||||
|
"prettifyLogMessage": false,
|
||||||
|
"showCommonLabels": false,
|
||||||
|
"showLabels": false,
|
||||||
|
"showTime": true,
|
||||||
|
"sortOrder": "Descending",
|
||||||
|
"wrapLogMessage": false
|
||||||
|
},
|
||||||
|
"pluginVersion": "11.3.0",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"editorMode": "builder",
|
||||||
|
"expr": "{service=\"api\"} | json",
|
||||||
|
"queryType": "range",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Logs",
|
||||||
|
"type": "logs"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"preload": false,
|
||||||
|
"schemaVersion": 40,
|
||||||
|
"tags": [],
|
||||||
|
"templating": {
|
||||||
|
"list": []
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-1h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "browser",
|
||||||
|
"title": "Logging",
|
||||||
|
"uid": "be3r0x46iwlq8f",
|
||||||
|
"version": 2,
|
||||||
|
"weekStart": "monday"
|
||||||
|
}
|
12
ci/development/grafana/dashboards/dashboard.yaml
Normal file
12
ci/development/grafana/dashboards/dashboard.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
providers:
|
||||||
|
- name: "Dashboard provider"
|
||||||
|
orgId: 1
|
||||||
|
type: file
|
||||||
|
disableDeletion: false
|
||||||
|
updateIntervalSeconds: 10
|
||||||
|
allowUiUpdates: false
|
||||||
|
options:
|
||||||
|
path: /var/lib/grafana/dashboards
|
||||||
|
foldersFromFilesStructure: true
|
11
ci/development/grafana/datasources/loki.yaml
Normal file
11
ci/development/grafana/datasources/loki.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
datasources:
|
||||||
|
- name: Loki
|
||||||
|
type: loki
|
||||||
|
access: proxy
|
||||||
|
orgId: 1
|
||||||
|
url: http://loki:3100
|
||||||
|
basicAuth: false
|
||||||
|
isDefault: true
|
||||||
|
version: 1
|
||||||
|
editable: false
|
@ -1,58 +0,0 @@
|
|||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:17-alpine
|
|
||||||
container_name: boring_postgres
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
environment:
|
|
||||||
- POSTGRES_PASSWORD=postgres
|
|
||||||
- POSTGRES_USER=postgres
|
|
||||||
- POSTGRES_DB=postgres
|
|
||||||
|
|
||||||
cache:
|
|
||||||
container_name: boring_redis
|
|
||||||
image: redis:7.4.1-alpine
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- '6379:6379'
|
|
||||||
# command: redis-server --save 20 1 --loglevel warning --requirepass development
|
|
||||||
|
|
||||||
loki:
|
|
||||||
image: grafana/loki:latest
|
|
||||||
ports:
|
|
||||||
- "9100:3100"
|
|
||||||
command: -config.file=/etc/loki/local-config.yaml
|
|
||||||
|
|
||||||
promtail:
|
|
||||||
image: grafana/promtail:latest
|
|
||||||
volumes:
|
|
||||||
- /var/log:/var/log
|
|
||||||
command: -config.file=/etc/promtail/config.yml
|
|
||||||
|
|
||||||
grafana:
|
|
||||||
environment:
|
|
||||||
- GF_PATHS_PROVISIONING=/etc/grafana/provisioning
|
|
||||||
- GF_AUTH_ANONYMOUS_ENABLED=true
|
|
||||||
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
|
|
||||||
entrypoint:
|
|
||||||
- sh
|
|
||||||
- -euc
|
|
||||||
- |
|
|
||||||
mkdir -p /etc/grafana/provisioning/datasources
|
|
||||||
cat <<EOF > /etc/grafana/provisioning/datasources/ds.yaml
|
|
||||||
apiVersion: 1
|
|
||||||
datasources:
|
|
||||||
- name: Loki
|
|
||||||
type: loki
|
|
||||||
access: proxy
|
|
||||||
orgId: 1
|
|
||||||
url: http://loki:3100
|
|
||||||
basicAuth: false
|
|
||||||
isDefault: true
|
|
||||||
version: 1
|
|
||||||
editable: false
|
|
||||||
EOF
|
|
||||||
/run.sh
|
|
||||||
image: grafana/grafana:latest
|
|
||||||
ports:
|
|
||||||
- "9000:3000"
|
|
@ -2,5 +2,5 @@ pre-commit:
|
|||||||
commands:
|
commands:
|
||||||
check:
|
check:
|
||||||
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc,astro}"
|
glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc,astro}"
|
||||||
run: bunx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
|
run: /run/current-system/sw/bin/biome check --apply --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files}
|
||||||
stage_fixed: true
|
stage_fixed: true
|
10
package.json
10
package.json
@ -6,6 +6,9 @@
|
|||||||
"check:apps": "bunx biome check --write --config-path ./biome.json ./apps",
|
"check:apps": "bunx biome check --write --config-path ./biome.json ./apps",
|
||||||
"check:packages": "bunx biome check --write --config-path ./biome.json ./packages",
|
"check:packages": "bunx biome check --write --config-path ./biome.json ./packages",
|
||||||
"dev": "bun --filter '*' dev",
|
"dev": "bun --filter '*' dev",
|
||||||
|
"dev:docker:up": "docker compose -f ci/development/docker-compose.yaml up -d",
|
||||||
|
"dev:docker:down": "docker compose -f ci/development/docker-compose.yaml down",
|
||||||
|
"dev:docker:stop": "docker compose -f ci/development/docker-compose.yaml stop",
|
||||||
"build": "bun --filter '*' build",
|
"build": "bun --filter '*' build",
|
||||||
"db:generate": "bun --filter '@boring.tools/database' db:generate",
|
"db:generate": "bun --filter '@boring.tools/database' db:generate",
|
||||||
"db:migrate": "bun --filter '@boring.tools/database' db:migrate",
|
"db:migrate": "bun --filter '@boring.tools/database' db:migrate",
|
||||||
@ -26,12 +29,9 @@
|
|||||||
"docker:page": "bun docker:page:build && bun docker:page:push"
|
"docker:page": "bun docker:page:build && bun docker:page:push"
|
||||||
},
|
},
|
||||||
"packageManager": "bun@1.1.29",
|
"packageManager": "bun@1.1.29",
|
||||||
"workspaces": [
|
"workspaces": ["apps/*", "packages/*"],
|
||||||
"apps/*",
|
|
||||||
"packages/*"
|
|
||||||
],
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.8.3",
|
"@biomejs/biome": "1.9.4",
|
||||||
"lefthook": "^1.7.15"
|
"lefthook": "^1.7.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"@logtail/node": "^0.5.0",
|
"@logtail/node": "^0.5.0",
|
||||||
"@logtail/winston": "^0.5.0",
|
"@logtail/winston": "^0.5.0",
|
||||||
"winston": "^3.14.2",
|
"winston": "^3.14.2",
|
||||||
|
"winston-console-format": "^1.0.8",
|
||||||
"winston-loki": "^6.1.3"
|
"winston-loki": "^6.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,20 +1,43 @@
|
|||||||
import { Logtail } from '@logtail/node'
|
import { Logtail } from '@logtail/node'
|
||||||
import { LogtailTransport } from '@logtail/winston'
|
import { LogtailTransport } from '@logtail/winston'
|
||||||
import winston from 'winston'
|
import { createLogger, format, transports } from 'winston'
|
||||||
|
import { consoleFormat } from 'winston-console-format'
|
||||||
import LokiTransport from 'winston-loki'
|
import LokiTransport from 'winston-loki'
|
||||||
|
|
||||||
// Create a Winston logger - passing in the Logtail transport
|
// Create a Winston logger - passing in the Logtail transport
|
||||||
export const logger = winston.createLogger({
|
export const logger = createLogger({
|
||||||
format: winston.format.json(),
|
format: format.combine(
|
||||||
|
format.timestamp(),
|
||||||
|
format.ms(),
|
||||||
|
format.errors({ stack: true }),
|
||||||
|
format.splat(),
|
||||||
|
format.json(),
|
||||||
|
),
|
||||||
transports: [
|
transports: [
|
||||||
new winston.transports.Console({
|
new transports.Console({
|
||||||
format: winston.format.json(),
|
silent: import.meta.env.NODE_ENV === 'test',
|
||||||
|
format: format.combine(
|
||||||
|
format.colorize({ all: true }),
|
||||||
|
format.padLevels(),
|
||||||
|
consoleFormat({
|
||||||
|
showMeta: true,
|
||||||
|
metaStrip: ['timestamp', 'service'],
|
||||||
|
inspectOptions: {
|
||||||
|
depth: Number.POSITIVE_INFINITY,
|
||||||
|
colors: true,
|
||||||
|
maxArrayLength: Number.POSITIVE_INFINITY,
|
||||||
|
breakLength: 120,
|
||||||
|
compact: Number.POSITIVE_INFINITY,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
new LokiTransport({
|
new LokiTransport({
|
||||||
|
silent: import.meta.env.NODE_ENV === 'test',
|
||||||
host: 'http://localhost:9100',
|
host: 'http://localhost:9100',
|
||||||
labels: { app: 'api' },
|
|
||||||
json: true,
|
json: true,
|
||||||
format: winston.format.json(),
|
labels: { service: import.meta.env.SERVICE_NAME ?? 'unknown' },
|
||||||
|
format: format.json(),
|
||||||
replaceTimestamp: true,
|
replaceTimestamp: true,
|
||||||
onConnectionError: (err) => console.error(err),
|
onConnectionError: (err) => console.error(err),
|
||||||
}),
|
}),
|
||||||
|
@ -7,8 +7,8 @@ export const PageOutput = z
|
|||||||
example: '',
|
example: '',
|
||||||
}),
|
}),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional().nullable(),
|
||||||
icon: z.string(),
|
icon: z.string().optional().nullable(),
|
||||||
changelogs: z.array(ChangelogOutput).optional(),
|
changelogs: z.array(ChangelogOutput).optional(),
|
||||||
})
|
})
|
||||||
.openapi('Page')
|
.openapi('Page')
|
||||||
|
@ -5,10 +5,10 @@ export const PageCreateInput = z
|
|||||||
title: z.string().min(3).openapi({
|
title: z.string().min(3).openapi({
|
||||||
example: 'My page',
|
example: 'My page',
|
||||||
}),
|
}),
|
||||||
description: z.string().optional().openapi({
|
description: z.string().optional().nullable().openapi({
|
||||||
example: '',
|
example: '',
|
||||||
}),
|
}),
|
||||||
icon: z.string().optional().openapi({
|
icon: z.string().optional().nullable().openapi({
|
||||||
example: 'base64...',
|
example: 'base64...',
|
||||||
}),
|
}),
|
||||||
changelogIds: z.array(z.string().uuid()),
|
changelogIds: z.array(z.string().uuid()),
|
||||||
|
@ -2,21 +2,23 @@ import { z } from '@hono/zod-openapi'
|
|||||||
|
|
||||||
export const PagePublicOutput = z.object({
|
export const PagePublicOutput = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
description: z.string(),
|
description: z.string().nullable(),
|
||||||
icon: z.string(),
|
icon: z.string(),
|
||||||
changelogs: z.array(
|
changelogs: z
|
||||||
z.object({
|
.array(
|
||||||
title: z.string(),
|
z.object({
|
||||||
description: z.string(),
|
title: z.string(),
|
||||||
versions: z.array(
|
description: z.string(),
|
||||||
z.object({
|
versions: z.array(
|
||||||
markdown: z.string(),
|
z.object({
|
||||||
version: z.string(),
|
markdown: z.string(),
|
||||||
releasedAt: z.date(),
|
version: z.string(),
|
||||||
}),
|
releasedAt: z.date(),
|
||||||
),
|
}),
|
||||||
}),
|
),
|
||||||
),
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const PagePublicParams = z.object({
|
export const PagePublicParams = z.object({
|
||||||
|
@ -7,10 +7,10 @@ export const PageUpdateInput = z
|
|||||||
title: z.string().min(3).optional().openapi({
|
title: z.string().min(3).optional().openapi({
|
||||||
example: 'My page',
|
example: 'My page',
|
||||||
}),
|
}),
|
||||||
description: z.string().optional().openapi({
|
description: z.string().nullable().optional().openapi({
|
||||||
example: '',
|
example: '',
|
||||||
}),
|
}),
|
||||||
icon: z.string().optional().openapi({
|
icon: z.string().optional().nullable().openapi({
|
||||||
example: 'base64...',
|
example: 'base64...',
|
||||||
}),
|
}),
|
||||||
changelogIds: z.array(z.string().uuid()).optional(),
|
changelogIds: z.array(z.string().uuid()).optional(),
|
||||||
|
Loading…
Reference in New Issue
Block a user