mirror of
https://github.com/SecurityBrewery/catalyst.git
synced 2025-12-06 15:22:47 +01:00
Compare commits
12 Commits
v0.13.2
...
v0.13.8-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2bdeecb0d | ||
|
|
42797509f7 | ||
|
|
70ba16a6bd | ||
|
|
f42de34780 | ||
|
|
88f56a2bdb | ||
|
|
88cc02b350 | ||
|
|
46f7815699 | ||
|
|
ea03a3ed23 | ||
|
|
6346140de5 | ||
|
|
d7bdf1d276 | ||
|
|
1e1022ab15 | ||
|
|
a2dd6c05e6 |
7
.github/workflows/goreleaser.yml
vendored
7
.github/workflows/goreleaser.yml
vendored
@@ -7,6 +7,8 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
@@ -21,6 +23,11 @@ jobs:
|
||||
|
||||
- run: make build-ui
|
||||
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: "securitybrewery"
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
|
||||
@@ -11,6 +11,14 @@ builds:
|
||||
- linux
|
||||
- darwin
|
||||
|
||||
dockers:
|
||||
- ids: [ catalyst ]
|
||||
dockerfile: docker/Dockerfile
|
||||
image_templates:
|
||||
- "ghcr.io/securitybrewery/catalyst:main"
|
||||
- "ghcr.io/securitybrewery/catalyst:latest"
|
||||
- "ghcr.io/securitybrewery/catalyst:{{.Tag}}"
|
||||
|
||||
archives:
|
||||
- format: tar.gz
|
||||
# this name template makes the OS and Arch compatible with the results of `uname`.
|
||||
|
||||
18
Makefile
18
Makefile
@@ -39,6 +39,22 @@ build-ui:
|
||||
cd ui && bun install
|
||||
cd ui && bun build-only
|
||||
|
||||
.PHONY: build
|
||||
build: build-ui
|
||||
@echo "Building..."
|
||||
go build -o catalyst .
|
||||
|
||||
|
||||
.PHONY: build-linux
|
||||
build-linux: build-ui
|
||||
@echo "Building..."
|
||||
GOOS=linux GOARCH=amd64 go build -o catalyst .
|
||||
|
||||
.PHONY: docker
|
||||
docker: build-linux
|
||||
@echo "Building Docker image..."
|
||||
docker build -f docker/Dockerfile -t catalyst .
|
||||
|
||||
.PHONY: dev
|
||||
dev:
|
||||
@echo "Running..."
|
||||
@@ -57,6 +73,6 @@ dev-10000:
|
||||
go run . fake-data --users 100 --tickets 10000
|
||||
go run . serve
|
||||
|
||||
.PHONY: dev-ui
|
||||
.PHONY: serve-ui
|
||||
serve-ui:
|
||||
cd ui && bun dev --port 3000
|
||||
|
||||
@@ -107,12 +107,7 @@ func setAppURL(app core.App) *cobra.Command {
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := app.Settings().Clone()
|
||||
if err != nil {
|
||||
app.Logger().Error(err.Error())
|
||||
|
||||
return
|
||||
}
|
||||
settings := app.Settings()
|
||||
|
||||
settings.Meta.AppUrl = args[0]
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ func addRoutes() func(*core.ServeEvent) error {
|
||||
return c.Redirect(http.StatusFound, "/ui/")
|
||||
})
|
||||
e.Router.GET("/ui/*", staticFiles())
|
||||
e.Router.GET("/health", func(c echo.Context) error {
|
||||
if _, err := Flags(e.App); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.String(http.StatusOK, "OK")
|
||||
})
|
||||
|
||||
e.Router.GET("/api/config", func(c echo.Context) error {
|
||||
flags, err := Flags(e.App)
|
||||
|
||||
14
docker/Dockerfile
Normal file
14
docker/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
RUN apt-get update && apt-get install -y curl python3 python3-pip python3-venv
|
||||
|
||||
COPY catalyst /usr/local/bin/catalyst
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
VOLUME /usr/local/bin/catalyst_data
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=3s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/health || exit 1
|
||||
|
||||
CMD ["/usr/local/bin/catalyst", "serve", "--http", "0.0.0.0:8080"]
|
||||
@@ -316,7 +316,7 @@ func reactionRecords(dao *daos.Dao) []*models.Record {
|
||||
|
||||
record := models.NewRecord(collection)
|
||||
record.SetId("w_" + security.PseudorandomString(10))
|
||||
record.Set("name", "Test Reaction")
|
||||
record.Set("name", "Alert Ingest Webhook")
|
||||
record.Set("trigger", "webhook")
|
||||
record.Set("triggerdata", triggerWebhook)
|
||||
record.Set("action", "python")
|
||||
@@ -334,7 +334,7 @@ func reactionRecords(dao *daos.Dao) []*models.Record {
|
||||
|
||||
record = models.NewRecord(collection)
|
||||
record.SetId("w_" + security.PseudorandomString(10))
|
||||
record.Set("name", "Test Reaction 2")
|
||||
record.Set("name", "Assign new Tickets")
|
||||
record.Set("trigger", "hook")
|
||||
record.Set("triggerdata", triggerHook)
|
||||
record.Set("action", "python")
|
||||
|
||||
1
go.mod
1
go.mod
@@ -11,6 +11,7 @@ require (
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
go.uber.org/multierr v1.11.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
2
go.sum
2
go.sum
@@ -229,6 +229,8 @@ go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D
|
||||
go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
|
||||
go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
|
||||
go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
gocloud.dev v0.37.0 h1:XF1rN6R0qZI/9DYjN16Uy0durAmSlf58DHOcb28GPro=
|
||||
gocloud.dev v0.37.0/go.mod h1:7/O4kqdInCNsc6LqgmuFnS0GRew4XNNYWpA44yQnwco=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
|
||||
43
migrations/8_dashboardview.go
Normal file
43
migrations/8_dashboardview.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
const dashboardCountsViewUpdateQuery = `SELECT id, count FROM (
|
||||
SELECT 'users' as id, COUNT(users.id) as count FROM users
|
||||
UNION
|
||||
SELECT 'tickets' as id, COUNT(tickets.id) as count FROM tickets
|
||||
UNION
|
||||
SELECT 'tasks' as id, COUNT(tasks.id) as count FROM tasks
|
||||
UNION
|
||||
SELECT 'reactions' as id, COUNT(reactions.id) as count FROM reactions
|
||||
) as counts;`
|
||||
|
||||
func dashboardCountsViewUpdateUp(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId(dashboardCountsViewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.Options = types.JsonMap{"query": dashboardCountsViewUpdateQuery}
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}
|
||||
|
||||
func dashboardCountsViewUpdateDown(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId(dashboardCountsViewName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.Options = types.JsonMap{"query": dashboardCountsViewQuery}
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}
|
||||
28
migrations/9_reactions_update.go
Normal file
28
migrations/9_reactions_update.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models/schema"
|
||||
)
|
||||
|
||||
func reactionsUpdateUp(db dbx.Builder) error {
|
||||
dao := daos.New(db)
|
||||
|
||||
triggers := []string{"webhook", "hook", "schedule"}
|
||||
|
||||
col, err := dao.FindCollectionByNameOrId(ReactionCollectionName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find collection %s: %w", ReactionCollectionName, err)
|
||||
}
|
||||
|
||||
field := col.Schema.GetFieldByName("trigger")
|
||||
|
||||
field.Options = &schema.SelectOptions{MaxSelect: 1, Values: triggers}
|
||||
|
||||
col.Schema.AddField(field)
|
||||
|
||||
return dao.SaveCollection(col)
|
||||
}
|
||||
@@ -12,4 +12,6 @@ func Register() {
|
||||
migrations.Register(reactionsUp, reactionsDown, "1700000005_reactions.go")
|
||||
migrations.Register(systemuserUp, systemuserDown, "1700000006_systemuser.go")
|
||||
migrations.Register(searchViewUp, searchViewDown, "1700000007_search_view.go")
|
||||
migrations.Register(dashboardCountsViewUpdateUp, dashboardCountsViewUpdateDown, "1700000008_dashboardview.go")
|
||||
migrations.Register(reactionsUpdateUp, nil, "1700000009_reactions_update.go")
|
||||
}
|
||||
|
||||
101
reaction/schedule/schedule.go
Normal file
101
reaction/schedule/schedule.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package schedule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase"
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/pocketbase/pocketbase/tools/cron"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/migrations"
|
||||
"github.com/SecurityBrewery/catalyst/reaction/action"
|
||||
)
|
||||
|
||||
type Schedule struct {
|
||||
Expression string `json:"expression"`
|
||||
}
|
||||
|
||||
func Start(pb *pocketbase.PocketBase) {
|
||||
scheduler := cron.New()
|
||||
|
||||
if err := scheduler.Add("reactions", "* * * * *", func() {
|
||||
ctx := context.Background()
|
||||
|
||||
moment := cron.NewMoment(time.Now())
|
||||
|
||||
if err := runSchedule(ctx, pb.App, moment); err != nil {
|
||||
slog.ErrorContext(ctx, fmt.Sprintf("failed to run hook reaction: %v", err))
|
||||
}
|
||||
}); err != nil {
|
||||
slog.Error(fmt.Sprintf("failed to add cron job: %v", err))
|
||||
}
|
||||
|
||||
scheduler.Start()
|
||||
}
|
||||
|
||||
func runSchedule(ctx context.Context, app core.App, moment *cron.Moment) error {
|
||||
var errs error
|
||||
|
||||
records, err := findByScheduleTrigger(app.Dao(), moment)
|
||||
if err != nil {
|
||||
errs = multierr.Append(errs, fmt.Errorf("failed to find schedule reaction: %w", err))
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, hook := range records {
|
||||
_, err = action.Run(ctx, app, hook.GetString("action"), hook.GetString("actiondata"), "{}")
|
||||
if err != nil {
|
||||
errs = multierr.Append(errs, fmt.Errorf("failed to run hook reaction: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func findByScheduleTrigger(dao *daos.Dao, moment *cron.Moment) ([]*models.Record, error) {
|
||||
records, err := dao.FindRecordsByExpr(migrations.ReactionCollectionName, dbx.HashExp{"trigger": "schedule"})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find schedule reaction: %w", err)
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var errs error
|
||||
|
||||
var matchedRecords []*models.Record
|
||||
|
||||
for _, record := range records {
|
||||
var schedule Schedule
|
||||
if err := json.Unmarshal([]byte(record.GetString("triggerdata")), &schedule); err != nil {
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
s, err := cron.NewSchedule(schedule.Expression)
|
||||
if err != nil {
|
||||
errs = multierr.Append(errs, err)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if s.IsDue(moment) {
|
||||
matchedRecords = append(matchedRecords, record)
|
||||
}
|
||||
}
|
||||
|
||||
return matchedRecords, errs
|
||||
}
|
||||
@@ -3,11 +3,13 @@ package reaction
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/reaction/schedule"
|
||||
"github.com/SecurityBrewery/catalyst/reaction/trigger/hook"
|
||||
"github.com/SecurityBrewery/catalyst/reaction/trigger/webhook"
|
||||
)
|
||||
|
||||
func BindHooks(pb *pocketbase.PocketBase, test bool) {
|
||||
schedule.Start(pb)
|
||||
hook.BindHooks(pb, test)
|
||||
webhook.BindHooks(pb)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
"github.com/SecurityBrewery/catalyst/migrations"
|
||||
"github.com/SecurityBrewery/catalyst/reaction/action"
|
||||
@@ -70,43 +71,49 @@ func runHook(ctx context.Context, app core.App, collection, event string, record
|
||||
return fmt.Errorf("failed to marshal webhook payload: %w", err)
|
||||
}
|
||||
|
||||
hook, found, err := findByHookTrigger(app.Dao(), collection, event)
|
||||
hooks, err := findByHookTrigger(app.Dao(), collection, event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find hook by trigger: %w", err)
|
||||
}
|
||||
|
||||
if !found {
|
||||
if len(hooks) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = action.Run(ctx, app, hook.GetString("action"), hook.GetString("actiondata"), string(payload))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to run hook reaction: %w", err)
|
||||
var errs error
|
||||
|
||||
for _, hook := range hooks {
|
||||
_, err = action.Run(ctx, app, hook.GetString("action"), hook.GetString("actiondata"), string(payload))
|
||||
if err != nil {
|
||||
errs = multierr.Append(errs, fmt.Errorf("failed to run hook reaction: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return errs
|
||||
}
|
||||
|
||||
func findByHookTrigger(dao *daos.Dao, collection, event string) (*models.Record, bool, error) {
|
||||
func findByHookTrigger(dao *daos.Dao, collection, event string) ([]*models.Record, error) {
|
||||
records, err := dao.FindRecordsByExpr(migrations.ReactionCollectionName, dbx.HashExp{"trigger": "hook"})
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to find hook reaction: %w", err)
|
||||
return nil, fmt.Errorf("failed to find hook reaction: %w", err)
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return nil, false, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var matchedRecords []*models.Record
|
||||
|
||||
for _, record := range records {
|
||||
var hook Hook
|
||||
if err := json.Unmarshal([]byte(record.GetString("triggerdata")), &hook); err != nil {
|
||||
return nil, false, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if slices.Contains(hook.Collections, collection) && slices.Contains(hook.Events, event) {
|
||||
return record, true, nil
|
||||
matchedRecords = append(matchedRecords, record)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, false, nil
|
||||
return matchedRecords, nil
|
||||
}
|
||||
|
||||
BIN
ui/bun.lockb
BIN
ui/bun.lockb
Binary file not shown.
@@ -7,7 +7,7 @@
|
||||
<title>Catalyst</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="h-screen w-screen"></div>
|
||||
<div id="app" class="h-screen w-screen overflow-hidden"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"easymde": "^2.18.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lucide-vue-next": "^0.365.0",
|
||||
"marked": "^12.0.2",
|
||||
"pinia": "^2.1.7",
|
||||
@@ -48,6 +49,7 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
|
||||
"@tsconfig/node20": "^20.1.2",
|
||||
"@types/lodash.debounce": "^4.0.9",
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
"@types/node": "^20.11.28",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
|
||||
@@ -7,17 +7,16 @@ defineProps<{
|
||||
isPending: boolean
|
||||
isError: boolean
|
||||
error: Error | null
|
||||
value: any
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="isPending" class="flex justify-center">
|
||||
<LoaderCircle class="h-16 w-16 animate-spin text-primary" />
|
||||
<div v-if="isPending" class="flex h-full w-full">
|
||||
<LoaderCircle class="m-auto h-16 w-16 animate-spin text-primary" />
|
||||
</div>
|
||||
<Alert v-else-if="isError" variant="destructive" class="mb-4">
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{{ error }}</AlertDescription>
|
||||
</Alert>
|
||||
<slot v-else-if="value" />
|
||||
<slot v-else />
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import PanelListElement from '@/components/common/PanelListElement.vue'
|
||||
import TanView from '@/components/TanView.vue'
|
||||
import PanelListElement from '@/components/layout/PanelListElement.vue'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
|
||||
@@ -32,24 +33,31 @@ const {
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<Card>
|
||||
<div v-if="tasks && tasks.length === 0" class="p-2 text-center text-sm text-gray-500">
|
||||
No open tasks
|
||||
</div>
|
||||
<PanelListElement v-else v-for="task in tasks" :key="task.id" class="pr-1">
|
||||
<span>{{ task.name }}</span>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'tickets',
|
||||
params: { type: task.expand.ticket.type, id: task.expand.ticket.id }
|
||||
}"
|
||||
:class="cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'ml-auto h-8')"
|
||||
>
|
||||
<span class="flex flex-row items-center text-sm text-gray-500">
|
||||
Go to {{ task.expand.ticket.name }}
|
||||
<ChevronRight class="ml-2 h-4 w-4" />
|
||||
</span>
|
||||
</RouterLink>
|
||||
</PanelListElement>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||
<div v-if="tasks && tasks.length === 0" class="p-2 text-center text-sm text-gray-500">
|
||||
No open tasks
|
||||
</div>
|
||||
<PanelListElement v-else v-for="task in tasks" :key="task.id" class="pr-1">
|
||||
<span>{{ task.name }}</span>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'tickets',
|
||||
params: { type: task.expand.ticket.type, id: task.expand.ticket.id }
|
||||
}"
|
||||
:class="
|
||||
cn(
|
||||
buttonVariants({ variant: 'outline', size: 'sm' }),
|
||||
'h-8 w-full sm:ml-auto sm:w-auto'
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="flex flex-row items-center text-sm text-gray-500">
|
||||
Go to {{ task.expand.ticket.name }}
|
||||
<ChevronRight class="ml-2 h-4 w-4" />
|
||||
</span>
|
||||
</RouterLink>
|
||||
</PanelListElement>
|
||||
</TanView>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import PanelListElement from '@/components/common/PanelListElement.vue'
|
||||
import PanelListElement from '@/components/layout/PanelListElement.vue'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
@@ -42,16 +42,21 @@ const age = (ticket: Ticket) =>
|
||||
</div>
|
||||
<PanelListElement v-else v-for="ticket in tickets" :key="ticket.id" class="gap-2 pr-1">
|
||||
<span>{{ ticket.name }}</span>
|
||||
<Separator orientation="vertical" class="h-4" />
|
||||
<Separator orientation="vertical" class="hidden h-4 sm:block" />
|
||||
<span class="text-sm text-muted-foreground">{{ ticket.expand.type.singular }}</span>
|
||||
<Separator orientation="vertical" class="h-4" />
|
||||
<Separator orientation="vertical" class="hidden h-4 sm:block" />
|
||||
<span class="text-sm text-muted-foreground">Open since {{ age(ticket) }} days</span>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: 'tickets',
|
||||
params: { type: ticket.type, id: ticket.id }
|
||||
}"
|
||||
:class="cn(buttonVariants({ variant: 'outline', size: 'sm' }), 'ml-auto h-8')"
|
||||
:class="
|
||||
cn(
|
||||
buttonVariants({ variant: 'outline', size: 'sm' }),
|
||||
'h-8 w-full sm:ml-auto sm:w-auto'
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="flex flex-row items-center text-sm text-gray-500">
|
||||
Go to {{ ticket.name }}
|
||||
|
||||
@@ -43,7 +43,7 @@ const ticketsPerWeek = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="tickets">
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||
<LineChart class="h-40" :data="ticketsPerWeek" index="week" :categories="['count']" />
|
||||
</TanView>
|
||||
</template>
|
||||
|
||||
@@ -30,7 +30,7 @@ const namedTypes = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="namedTypes">
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||
<div v-if="namedTypes" class="flex flex-1 items-center">
|
||||
<DonutChart index="plural" type="donut" category="count" :data="namedTypes" />
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
SelectValue
|
||||
} from '@/components/ui/select'
|
||||
|
||||
import isEqual from 'lodash.isequal'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
import type { JSONSchema } from '@/lib/types'
|
||||
@@ -34,6 +35,11 @@ onMounted(() => {
|
||||
watch(
|
||||
() => formdata.value,
|
||||
() => {
|
||||
const normFormdata = JSON.parse(JSON.stringify(formdata.value))
|
||||
const normModel = JSON.parse(JSON.stringify(model.value))
|
||||
|
||||
if (isEqual(normFormdata, normModel)) return
|
||||
|
||||
model.value = { ...formdata.value }
|
||||
},
|
||||
{ deep: true }
|
||||
|
||||
@@ -3,9 +3,6 @@ import ShortCut from '@/components/ShortCut.vue'
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
// import { Textarea } from '@/components/ui/textarea'
|
||||
// import { Input } from '@/components/ui/input'
|
||||
|
||||
const model = defineModel({
|
||||
type: String
|
||||
})
|
||||
|
||||
5
ui/src/components/layout/ColumnBody.vue
Normal file
5
ui/src/components/layout/ColumnBody.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-1 items-start justify-start overflow-y-auto overflow-x-hidden">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
13
ui/src/components/layout/ColumnBodyContainer.vue
Normal file
13
ui/src/components/layout/ColumnBodyContainer.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineProps<{
|
||||
small?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('mx-auto flex w-full max-w-[72rem] gap-4 p-4', small && 'max-w-[47rem]')">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
25
ui/src/components/layout/ColumnHeader.vue
Normal file
25
ui/src/components/layout/ColumnHeader.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineProps<{
|
||||
title?: string
|
||||
nowrap?: boolean
|
||||
hideSeparator?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn('flex min-h-14 flex-wrap items-center gap-2 bg-background p-2', nowrap && 'flex-nowrap')
|
||||
"
|
||||
>
|
||||
<h1 v-if="title" class="text-xl font-bold">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<slot />
|
||||
</div>
|
||||
<Separator v-if="!hideSeparator" />
|
||||
</template>
|
||||
@@ -12,7 +12,7 @@ const props = defineProps<{
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex w-full items-center border-t px-2 py-1 first:rounded-t first:border-none last:rounded-b',
|
||||
'flex w-full flex-col items-start border-t px-2 py-1 first:rounded-t first:border-none last:rounded-b sm:flex-row sm:items-center',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
@@ -8,57 +8,78 @@ import { Separator } from '@/components/ui/separator'
|
||||
|
||||
import { Menu } from 'lucide-vue-next'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useCatalystStore } from '@/store/catalyst'
|
||||
|
||||
const catalystStore = useCatalystStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-[57px] items-center border-b bg-background">
|
||||
<CatalystLogo
|
||||
class="size-8"
|
||||
:class="{ 'flex-1': catalystStore.sidebarCollapsed, 'mx-3': !catalystStore.sidebarCollapsed }"
|
||||
/>
|
||||
<h1 class="text-xl font-bold" v-if="!catalystStore.sidebarCollapsed">Catalyst</h1>
|
||||
</div>
|
||||
<NavList
|
||||
:is-collapsed="catalystStore.sidebarCollapsed"
|
||||
:links="[
|
||||
{
|
||||
title: 'Dashboard',
|
||||
icon: 'PanelsTopLeft',
|
||||
variant: 'ghost',
|
||||
to: '/dashboard'
|
||||
}
|
||||
]"
|
||||
/>
|
||||
<Separator />
|
||||
<IncidentNav :is-collapsed="catalystStore.sidebarCollapsed" />
|
||||
|
||||
<div class="flex-1" />
|
||||
|
||||
<Separator />
|
||||
<NavList
|
||||
:is-collapsed="catalystStore.sidebarCollapsed"
|
||||
:links="[
|
||||
{
|
||||
title: 'Reactions',
|
||||
icon: 'Zap',
|
||||
variant: 'ghost',
|
||||
to: '/reactions'
|
||||
}
|
||||
]"
|
||||
/>
|
||||
<Separator />
|
||||
<UserDropDown :is-collapsed="catalystStore.sidebarCollapsed" />
|
||||
<Separator />
|
||||
<Button
|
||||
variant="ghost"
|
||||
@click="catalystStore.toggleSidebar()"
|
||||
size="sm"
|
||||
class="m-2 justify-start px-3.5"
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex min-w-48 shrink-0 flex-col border-r bg-popover', // transition-all duration-300 ease-in-out',
|
||||
catalystStore.sidebarCollapsed && 'min-w-[50px]'
|
||||
)
|
||||
"
|
||||
>
|
||||
<Menu class="size-4" />
|
||||
<span v-if="!catalystStore.sidebarCollapsed" class="ml-2">Toggle Sidebar</span>
|
||||
</Button>
|
||||
<div class="flex h-[57px] items-center border-b bg-background">
|
||||
<CatalystLogo
|
||||
class="size-8"
|
||||
:class="{
|
||||
'flex-1': catalystStore.sidebarCollapsed,
|
||||
'mx-3': !catalystStore.sidebarCollapsed
|
||||
}"
|
||||
/>
|
||||
<h1 class="text-xl font-bold" v-if="!catalystStore.sidebarCollapsed">Catalyst</h1>
|
||||
</div>
|
||||
<NavList
|
||||
:is-collapsed="catalystStore.sidebarCollapsed"
|
||||
:links="[
|
||||
{
|
||||
title: 'Dashboard',
|
||||
icon: 'PanelsTopLeft',
|
||||
variant: 'ghost',
|
||||
to: '/dashboard'
|
||||
}
|
||||
]"
|
||||
/>
|
||||
<Separator />
|
||||
<IncidentNav :is-collapsed="catalystStore.sidebarCollapsed" />
|
||||
|
||||
<div class="flex-1" />
|
||||
|
||||
<Separator />
|
||||
<NavList
|
||||
:is-collapsed="catalystStore.sidebarCollapsed"
|
||||
:links="[
|
||||
{
|
||||
title: 'Reactions',
|
||||
icon: 'Zap',
|
||||
variant: 'ghost',
|
||||
to: '/reactions'
|
||||
}
|
||||
]"
|
||||
/>
|
||||
<Separator />
|
||||
<UserDropDown :is-collapsed="catalystStore.sidebarCollapsed" />
|
||||
<Separator />
|
||||
<div :class="cn('flex h-14 items-center px-3', !catalystStore.sidebarCollapsed && 'px-2')">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@click="catalystStore.toggleSidebar()"
|
||||
size="default"
|
||||
:class="
|
||||
cn(
|
||||
'p-0',
|
||||
catalystStore.sidebarCollapsed && 'w-9',
|
||||
!catalystStore.sidebarCollapsed && 'w-full justify-start px-3'
|
||||
)
|
||||
"
|
||||
>
|
||||
<Menu class="size-4" />
|
||||
<span v-if="!catalystStore.sidebarCollapsed" class="ml-2">Toggle Sidebar</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,29 +3,37 @@ import SideBar from '@/components/layout/SideBar.vue'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useCatalystStore } from '@/store/catalyst'
|
||||
|
||||
const catalystStore = useCatalystStore()
|
||||
defineProps<{
|
||||
showDetails?: boolean
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TooltipProvider :delay-duration="0">
|
||||
<div class="flex h-full flex-row items-stretch bg-muted/40">
|
||||
<SideBar />
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex min-w-48 flex-col border-r bg-popover', // transition-all duration-300 ease-in-out',
|
||||
catalystStore.sidebarCollapsed && 'min-w-[50px]'
|
||||
'w-full flex-initial border-r sm:w-72',
|
||||
!showDetails && 'flex',
|
||||
showDetails && 'hidden sm:flex'
|
||||
)
|
||||
"
|
||||
>
|
||||
<SideBar />
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<slot name="list" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-72 flex-initial border-r">
|
||||
<slot name="list" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<slot name="single" />
|
||||
<div
|
||||
:class="
|
||||
cn('flex-1 overflow-hidden', !showDetails && 'hidden sm:flex', showDetails && 'flex')
|
||||
"
|
||||
>
|
||||
<div class="flex h-full w-full flex-1 flex-col">
|
||||
<slot name="single" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -1,27 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import SideBar from '@/components/layout/SideBar.vue'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useCatalystStore } from '@/store/catalyst'
|
||||
|
||||
const catalystStore = useCatalystStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TooltipProvider :delay-duration="0">
|
||||
<div class="flex h-full flex-row items-stretch bg-muted/40">
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex min-w-48 flex-col border-r bg-popover', // transition-all duration-300 ease-in-out',
|
||||
catalystStore.sidebarCollapsed && 'min-w-[50px]'
|
||||
)
|
||||
"
|
||||
>
|
||||
<SideBar />
|
||||
<SideBar />
|
||||
<div class="flex h-full w-full flex-col">
|
||||
<slot />
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import TanView from '@/components/TanView.vue'
|
||||
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
||||
import ColumnBody from '@/components/layout/ColumnBody.vue'
|
||||
import ColumnBodyContainer from '@/components/layout/ColumnBodyContainer.vue'
|
||||
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||
import ReactionForm from '@/components/reaction/ReactionForm.vue'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { toast } from '@/components/ui/toast'
|
||||
|
||||
import { ChevronLeft } from 'lucide-vue-next'
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -72,28 +76,29 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="reaction">
|
||||
<div class="flex h-full flex-1 flex-col overflow-hidden">
|
||||
<div class="flex items-center bg-background px-4 py-2">
|
||||
<div class="ml-auto">
|
||||
<DeleteDialog
|
||||
v-if="reaction"
|
||||
collection="reactions"
|
||||
:id="reaction.id"
|
||||
:name="reaction.name"
|
||||
:singular="'Reaction'"
|
||||
:to="{ name: 'reactions' }"
|
||||
:queryKey="['reactions']"
|
||||
/>
|
||||
</div>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||
<ColumnHeader>
|
||||
<Button @click="router.push({ name: 'reactions' })" variant="outline" class="sm:hidden">
|
||||
<ChevronLeft class="mr-2 size-4" />
|
||||
Back
|
||||
</Button>
|
||||
<div class="ml-auto">
|
||||
<DeleteDialog
|
||||
v-if="reaction"
|
||||
collection="reactions"
|
||||
:id="reaction.id"
|
||||
:name="reaction.name"
|
||||
:singular="'Reaction'"
|
||||
:to="{ name: 'reactions' }"
|
||||
:queryKey="['reactions']"
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
</ColumnHeader>
|
||||
|
||||
<ScrollArea v-if="reaction" class="flex-1">
|
||||
<div class="flex max-w-[640px] flex-col gap-4 p-4">
|
||||
<ReactionForm :reaction="reaction" @submit="updateReactionMutation.mutate" />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<ColumnBody v-if="reaction">
|
||||
<ColumnBodyContainer small>
|
||||
<ReactionForm :reaction="reaction" @submit="updateReactionMutation.mutate" />
|
||||
</ColumnBodyContainer>
|
||||
</ColumnBody>
|
||||
</TanView>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import ActionPythonFormFields from '@/components/reaction/ActionPythonFormFields.vue'
|
||||
import ActionWebhookFormFields from '@/components/reaction/ActionWebhookFormFields.vue'
|
||||
import TriggerHookFormFields from '@/components/reaction/TriggerHookFormFields.vue'
|
||||
import TriggerScheduleFormFields from '@/components/reaction/TriggerScheduleFormFields.vue'
|
||||
import TriggerWebhookFormFields from '@/components/reaction/TriggerWebhookFormFields.vue'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -67,6 +68,25 @@ defineRule('required', (value: string) => {
|
||||
return true
|
||||
})
|
||||
|
||||
defineRule('triggerdata.expression', (value: string) => {
|
||||
if (values.trigger !== 'schedule') {
|
||||
return true
|
||||
}
|
||||
if (!value) {
|
||||
return 'This field is required'
|
||||
}
|
||||
const macros = ['@yearly', '@annually', '@monthly', '@weekly', '@daily', '@midnight', '@hourly']
|
||||
if (macros.includes(value)) {
|
||||
return true
|
||||
}
|
||||
const expression =
|
||||
/^(\*|([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])|\*\/([0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])) (\*|([0-9]|1[0-9]|2[0-3])|\*\/([0-9]|1[0-9]|2[0-3])) (\*|([1-9]|1[0-9]|2[0-9]|3[0-1])|\*\/([1-9]|1[0-9]|2[0-9]|3[0-1])) (\*|([1-9]|1[0-2])|\*\/([1-9]|1[0-2])) (\*|([0-6])|\*\/([0-6]))$/
|
||||
if (value.match(expression)) {
|
||||
return true
|
||||
}
|
||||
return 'Invalid cron expression'
|
||||
})
|
||||
|
||||
defineRule('triggerdata.token', (value: string) => {
|
||||
return true
|
||||
})
|
||||
@@ -160,6 +180,7 @@ const { handleSubmit, validate, values } = useForm({
|
||||
validationSchema: {
|
||||
name: 'required',
|
||||
trigger: 'required',
|
||||
'triggerdata.expression': 'triggerdata.expression',
|
||||
'triggerdata.token': 'triggerdata.token',
|
||||
'triggerdata.path': 'triggerdata.path',
|
||||
'triggerdata.collections': 'triggerdata.collections',
|
||||
@@ -227,7 +248,7 @@ const curlExample = computed(() => {
|
||||
let cmd = `curl`
|
||||
|
||||
if (values.triggerdata.token) {
|
||||
cmd += ` -H "Auth: Bearer ${values.triggerdata.token}"`
|
||||
cmd += ` -H "Authorization: Bearer ${values.triggerdata.token}"`
|
||||
}
|
||||
|
||||
if (values.triggerdata.path) {
|
||||
@@ -239,7 +260,7 @@ const curlExample = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit="onSubmit" class="flex flex-col items-start gap-4">
|
||||
<form @submit="onSubmit" class="flex w-full flex-col items-start gap-4">
|
||||
<FormField name="name" v-slot="{ componentField }" validate-on-input>
|
||||
<FormItem class="w-full">
|
||||
<FormLabel for="name" class="text-right">Name</FormLabel>
|
||||
@@ -263,6 +284,7 @@ const curlExample = computed(() => {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="schedule">Schedule</SelectItem>
|
||||
<SelectItem value="webhook">HTTP / Webhook</SelectItem>
|
||||
<SelectItem value="hook">Collection Hook</SelectItem>
|
||||
</SelectGroup>
|
||||
@@ -277,7 +299,8 @@ const curlExample = computed(() => {
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<TriggerWebhookFormFields v-if="values.trigger === 'webhook'" />
|
||||
<TriggerScheduleFormFields v-if="values.trigger === 'schedule'" />
|
||||
<TriggerWebhookFormFields v-else-if="values.trigger === 'webhook'" />
|
||||
<TriggerHookFormFields v-else-if="values.trigger === 'hook'" />
|
||||
|
||||
<div v-if="values.trigger === 'webhook'">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import TanView from '@/components/TanView.vue'
|
||||
import ResourceListElement from '@/components/common/ResourceListElement.vue'
|
||||
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||
import ResourceListElement from '@/components/layout/ResourceListElement.vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
import { useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { onMounted } from 'vue'
|
||||
@@ -32,7 +32,9 @@ const subtitle = (reaction: Reaction) =>
|
||||
triggerNiceName(reaction) + ' to ' + reactionNiceName(reaction)
|
||||
|
||||
const triggerNiceName = (reaction: Reaction) => {
|
||||
if (reaction.trigger === 'hook') {
|
||||
if (reaction.trigger === 'schedule') {
|
||||
return 'Schedule'
|
||||
} else if (reaction.trigger === 'hook') {
|
||||
return 'Collection Hook'
|
||||
} else if (reaction.trigger === 'webhook') {
|
||||
return 'HTTP / Webhook'
|
||||
@@ -63,32 +65,28 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="reactions">
|
||||
<div class="flex h-screen flex-col">
|
||||
<div class="flex items-center bg-background px-4 py-2">
|
||||
<h1 class="text-xl font-bold">Reactions</h1>
|
||||
<div class="ml-auto">
|
||||
<Button variant="ghost" @click="openNew"> New Reaction</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="mt-2 flex flex-1 flex-col gap-2 p-4 pt-0">
|
||||
<TransitionGroup name="list" appear>
|
||||
<ResourceListElement
|
||||
v-for="reaction in reactions"
|
||||
:key="reaction.id"
|
||||
:title="reaction.name"
|
||||
:created="reaction.created"
|
||||
:subtitle="subtitle(reaction)"
|
||||
description=""
|
||||
:active="route.params.id === reaction.id"
|
||||
:to="{ name: 'reactions', params: { id: reaction.id } }"
|
||||
:open="false"
|
||||
>
|
||||
{{ reaction.name }}
|
||||
</ResourceListElement>
|
||||
</TransitionGroup>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||
<ColumnHeader title="Reactions">
|
||||
<div class="ml-auto">
|
||||
<Button variant="ghost" @click="openNew">New Reaction</Button>
|
||||
</div>
|
||||
</ColumnHeader>
|
||||
<div class="mt-2 flex flex-1 flex-col gap-2 p-2 pt-0">
|
||||
<TransitionGroup name="list" appear>
|
||||
<ResourceListElement
|
||||
v-for="reaction in reactions"
|
||||
:key="reaction.id"
|
||||
:title="reaction.name"
|
||||
:created="reaction.created"
|
||||
:subtitle="subtitle(reaction)"
|
||||
description=""
|
||||
:active="route.params.id === reaction.id"
|
||||
:to="{ name: 'reactions', params: { id: reaction.id } }"
|
||||
:open="false"
|
||||
>
|
||||
{{ reaction.name }}
|
||||
</ResourceListElement>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</TanView>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import ColumnBody from '@/components/layout/ColumnBody.vue'
|
||||
import ColumnBodyContainer from '@/components/layout/ColumnBodyContainer.vue'
|
||||
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||
import ReactionForm from '@/components/reaction/ReactionForm.vue'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { ChevronLeft } from 'lucide-vue-next'
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -24,14 +28,16 @@ const addReactionMutation = useMutation({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-1 flex-col overflow-hidden">
|
||||
<div class="flex min-h-14 items-center bg-background px-4 py-2"></div>
|
||||
<Separator />
|
||||
<ColumnHeader>
|
||||
<Button @click="router.push({ name: 'reactions' })" variant="outline" class="sm:hidden">
|
||||
<ChevronLeft class="mr-2 size-4" />
|
||||
Back
|
||||
</Button>
|
||||
</ColumnHeader>
|
||||
|
||||
<ScrollArea class="flex-1">
|
||||
<div class="flex max-w-[640px] flex-col gap-4 p-4">
|
||||
<ReactionForm @submit="addReactionMutation.mutate" />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<ColumnBody>
|
||||
<ColumnBodyContainer small>
|
||||
<ReactionForm @submit="addReactionMutation.mutate" />
|
||||
</ColumnBodyContainer>
|
||||
</ColumnBody>
|
||||
</template>
|
||||
|
||||
42
ui/src/components/reaction/TriggerScheduleFormFields.vue
Normal file
42
ui/src/components/reaction/TriggerScheduleFormFields.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@/components/ui/form'
|
||||
import { Input } from '@/components/ui/input'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormField name="triggerdata.expression" v-slot="{ componentField }" validate-on-input>
|
||||
<FormItem>
|
||||
<FormLabel for="expression" class="text-right"> Cron Expression </FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="expression"
|
||||
class="col-span-3"
|
||||
v-bind="componentField"
|
||||
placeholder="Enter a cron expression"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
<p class="text-sm text-gray-500">
|
||||
A cron expression or macro to schedule the trigger. Example: <code>0 * * * *</code> (every
|
||||
hour) or <code>@daily</code> (every day).
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/Cron#CRON_expression"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-blue-600 underline"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</template>
|
||||
@@ -15,15 +15,11 @@ import { Input } from '@/components/ui/input'
|
||||
<FormItem>
|
||||
<FormLabel for="token" class="text-right">Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="token"
|
||||
class="col-span-3"
|
||||
v-bind="componentField"
|
||||
placeholder="Enter a token (e.g. 'xyz...')"
|
||||
/>
|
||||
<Input id="token" class="col-span-3" v-bind="componentField" placeholder="Enter a token" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Optional. Include an authorization token in the request headers.
|
||||
Optional. Include an authorization token in the request headers. Example:
|
||||
<code>Bearer YOUR_TOKEN</code>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -33,14 +29,11 @@ import { Input } from '@/components/ui/input'
|
||||
<FormItem>
|
||||
<FormLabel for="path" class="text-right">Path</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
id="path"
|
||||
class="col-span-3"
|
||||
v-bind="componentField"
|
||||
placeholder="Enter a path (e.g. 'action1')"
|
||||
/>
|
||||
<Input id="path" class="col-span-3" v-bind="componentField" placeholder="Enter a path" />
|
||||
</FormControl>
|
||||
<FormDescription> Specify the path to trigger the reaction. </FormDescription>
|
||||
<FormDescription>
|
||||
Specify the path to trigger the reaction. Example: <code>action1</code>
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
||||
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||
import TicketCloseDialog from '@/components/ticket/TicketCloseDialog.vue'
|
||||
import TicketUserSelect from '@/components/ticket/TicketUserSelect.vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
|
||||
import { Check, CircleDot, Repeat } from 'lucide-vue-next'
|
||||
import { Check, ChevronLeft, CircleDot, Repeat } from 'lucide-vue-next'
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, ref } from 'vue'
|
||||
@@ -49,7 +50,7 @@ const changeTypeMutation = useMutation({
|
||||
}),
|
||||
onSuccess: (data: Ticket) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tickets'] })
|
||||
router.push({ name: 'tickets', params: { type: data.type, id: props.ticket.id } })
|
||||
// router.push({ name: 'tickets', params: { type: data.type, id: props.ticket.id } })
|
||||
},
|
||||
onError: handleError
|
||||
})
|
||||
@@ -74,74 +75,81 @@ const closeTicketDialogOpen = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between bg-background p-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="outline" :disabled="!ticket">
|
||||
<Icon :name="ticket.expand.type.icon" class="mr-2 size-4" />
|
||||
{{ ticket.expand.type.singular }}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
v-for="type in otherTypes"
|
||||
:key="type.id"
|
||||
class="cursor-pointer"
|
||||
@click="changeTypeMutation.mutate(type.id)"
|
||||
>
|
||||
<Icon :name="type.icon" class="mr-2 size-4" />
|
||||
Convert to {{ type.singular }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Change Type</TooltipContent>
|
||||
</Tooltip>
|
||||
<TicketCloseDialog v-model="closeTicketDialogOpen" :ticket="ticket" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="outline" :disabled="!ticket">
|
||||
<CircleDot v-if="ticket.open" class="mr-2 h-4 w-4" />
|
||||
<Check v-else class="mr-2 h-4 w-4" />
|
||||
{{ ticket?.open ? 'Open' : 'Closed' }}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
v-if="ticket.open"
|
||||
class="cursor-pointer"
|
||||
@click="closeTicketDialogOpen = true"
|
||||
>
|
||||
<Check class="mr-2 size-4" />
|
||||
Close Ticket
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem v-else class="cursor-pointer" @click="closeTicketMutation.mutate">
|
||||
<Repeat class="mr-2 size-4" />
|
||||
Reopen Ticket
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Change Status</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div>
|
||||
<TicketUserSelect :key="ticket.owner" :uID="ticket.owner" :ticket="ticket" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Change User</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ColumnHeader>
|
||||
<Button
|
||||
@click="router.push({ name: 'tickets', params: { type: ticket.type } })"
|
||||
variant="outline"
|
||||
class="sm:hidden"
|
||||
>
|
||||
<ChevronLeft class="mr-2 size-4" />
|
||||
Back
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="outline" :disabled="!ticket">
|
||||
<Icon :name="ticket.expand.type.icon" class="mr-2 size-4" />
|
||||
{{ ticket.expand.type.singular }}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
v-for="type in otherTypes"
|
||||
:key="type.id"
|
||||
class="cursor-pointer"
|
||||
@click="changeTypeMutation.mutate(type.id)"
|
||||
>
|
||||
<Icon :name="type.icon" class="mr-2 size-4" />
|
||||
Convert to {{ type.singular }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Change Type</TooltipContent>
|
||||
</Tooltip>
|
||||
<TicketCloseDialog v-model="closeTicketDialogOpen" :ticket="ticket" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="outline" :disabled="!ticket">
|
||||
<CircleDot v-if="ticket.open" class="mr-2 h-4 w-4" />
|
||||
<Check v-else class="mr-2 h-4 w-4" />
|
||||
{{ ticket?.open ? 'Open' : 'Closed' }}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem
|
||||
v-if="ticket.open"
|
||||
class="cursor-pointer"
|
||||
@click="closeTicketDialogOpen = true"
|
||||
>
|
||||
<Check class="mr-2 size-4" />
|
||||
Close Ticket
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem v-else class="cursor-pointer" @click="closeTicketMutation.mutate">
|
||||
<Repeat class="mr-2 size-4" />
|
||||
Reopen Ticket
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Change Status</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<div>
|
||||
<TicketUserSelect :key="ticket.owner" :uID="ticket.owner" :ticket="ticket" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Change User</TooltipContent>
|
||||
</Tooltip>
|
||||
<div class="-mx-1 flex-1" />
|
||||
<DeleteDialog
|
||||
v-if="ticket"
|
||||
:collection="'tickets'"
|
||||
@@ -151,5 +159,5 @@ const closeTicketDialogOpen = ref(false)
|
||||
:to="{ name: 'tickets' }"
|
||||
:queryKey="['tickets']"
|
||||
/>
|
||||
</div>
|
||||
</ColumnHeader>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
|
||||
@@ -38,7 +39,7 @@ const closeButtonDisabled = false // computed(() => !props.ticket.open || messag
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between gap-2 bg-background p-2">
|
||||
<ColumnHeader nowrap hideSeparator>
|
||||
<Input v-if="ticket.open" v-model="resolution" placeholder="Closing reason" />
|
||||
<div v-else class="flex-1">
|
||||
<p class="ml-2 text-gray-500">Closed: {{ ticket.resolution }}</p>
|
||||
@@ -56,5 +57,5 @@ const closeButtonDisabled = false // computed(() => !props.ticket.open || messag
|
||||
: 'Reopen ' + props.ticket.expand.type.singular
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
</ColumnHeader>
|
||||
</template>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import TanView from '@/components/TanView.vue'
|
||||
import JSONSchemaFormFields from '@/components/form/JSONSchemaFormFields.vue'
|
||||
import DynamicMDEditor from '@/components/input/DynamicMDEditor.vue'
|
||||
import ColumnBody from '@/components/layout/ColumnBody.vue'
|
||||
import ColumnBodyContainer from '@/components/layout/ColumnBodyContainer.vue'
|
||||
import StatusIcon from '@/components/ticket/StatusIcon.vue'
|
||||
import TicketActionBar from '@/components/ticket/TicketActionBar.vue'
|
||||
import TicketCloseBar from '@/components/ticket/TicketCloseBar.vue'
|
||||
@@ -15,14 +17,13 @@ import TicketTimeline from '@/components/ticket/timeline/TicketTimeline.vue'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
|
||||
import { Edit } from 'lucide-vue-next'
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { pb } from '@/lib/pocketbase'
|
||||
@@ -100,87 +101,97 @@ const updateDescription = (value: string) => (message.value = value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="ticket">
|
||||
<div v-if="ticket" class="flex h-full flex-col">
|
||||
<TicketActionBar :ticket="ticket" />
|
||||
<Separator />
|
||||
<div class="flex w-full max-w-7xl flex-1 flex-col overflow-hidden xl:m-auto xl:flex-row">
|
||||
<div class="flex flex-1 flex-col gap-4 px-4 pt-4">
|
||||
<TicketHeader :ticket="ticket" />
|
||||
<Card class="relative p-4">
|
||||
<Button v-if="!editMode" variant="outline" class="float-right h-8 gap-2" @click="edit">
|
||||
<Edit class="h-3.5 w-3.5" />
|
||||
<span>Edit</span>
|
||||
</Button>
|
||||
<DynamicMDEditor
|
||||
:modelValue="ticket.description"
|
||||
@update:modelValue="updateDescription"
|
||||
v-model:edit="editMode"
|
||||
autofocus
|
||||
placeholder="Type a description..."
|
||||
@save="editDescriptionMutation.mutate"
|
||||
class="min-h-14"
|
||||
/>
|
||||
</Card>
|
||||
<Separator />
|
||||
<Tabs default-value="timeline" class="flex flex-1 flex-col overflow-hidden">
|
||||
<TabsList>
|
||||
<TabsTrigger value="timeline">
|
||||
Timeline
|
||||
<Badge
|
||||
v-if="
|
||||
ticket.expand.timeline_via_ticket &&
|
||||
ticket.expand.timeline_via_ticket.length > 0
|
||||
"
|
||||
variant="outline"
|
||||
class="ml-2"
|
||||
>
|
||||
{{
|
||||
ticket.expand.timeline_via_ticket ? ticket.expand.timeline_via_ticket.length : 0
|
||||
}}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="tasks">
|
||||
Tasks
|
||||
<Badge
|
||||
v-if="ticket.expand.tasks_via_ticket && ticket.expand.tasks_via_ticket.length > 0"
|
||||
variant="outline"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ ticket.expand.tasks_via_ticket ? ticket.expand.tasks_via_ticket.length : 0 }}
|
||||
<StatusIcon :status="taskStatus" class="size-6" />
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="comments">
|
||||
Comments
|
||||
<Badge
|
||||
v-if="
|
||||
ticket.expand.comments_via_ticket &&
|
||||
ticket.expand.comments_via_ticket.length > 0
|
||||
"
|
||||
variant="outline"
|
||||
class="ml-2"
|
||||
>
|
||||
{{
|
||||
ticket.expand.comments_via_ticket ? ticket.expand.comments_via_ticket.length : 0
|
||||
}}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TicketTab value="timeline">
|
||||
<TicketTimeline :ticket="ticket" :timeline="ticket.expand.timeline_via_ticket" />
|
||||
</TicketTab>
|
||||
<TicketTab value="tasks">
|
||||
<TicketTasks :ticket="ticket" :tasks="ticket.expand.tasks_via_ticket" />
|
||||
</TicketTab>
|
||||
<TicketTab value="comments">
|
||||
<TicketComments :ticket="ticket" :comments="ticket.expand.comments_via_ticket" />
|
||||
</TicketTab>
|
||||
</Tabs>
|
||||
<Separator class="xl:hidden" />
|
||||
</div>
|
||||
<ScrollArea>
|
||||
<div class="flex flex-initial flex-col gap-4 p-4 xl:w-96">
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||
<template v-if="ticket">
|
||||
<TicketActionBar :ticket="ticket" class="shrink-0" />
|
||||
<ColumnBody>
|
||||
<ColumnBodyContainer class="flex-col gap-4 xl:flex-row">
|
||||
<div class="flex flex-1 flex-col gap-4">
|
||||
<TicketHeader :ticket="ticket" />
|
||||
<Card class="relative p-4">
|
||||
<Button
|
||||
v-if="!editMode"
|
||||
variant="outline"
|
||||
class="float-right h-8 gap-2"
|
||||
@click="edit"
|
||||
>
|
||||
<Edit class="h-3.5 w-3.5" />
|
||||
<span>Edit</span>
|
||||
</Button>
|
||||
<DynamicMDEditor
|
||||
:modelValue="ticket.description"
|
||||
@update:modelValue="updateDescription"
|
||||
v-model:edit="editMode"
|
||||
autofocus
|
||||
placeholder="Type a description..."
|
||||
@save="editDescriptionMutation.mutate"
|
||||
class="min-h-14"
|
||||
/>
|
||||
</Card>
|
||||
<Separator />
|
||||
<Tabs default-value="timeline" class="flex flex-1 flex-col">
|
||||
<TabsList>
|
||||
<TabsTrigger value="timeline">
|
||||
Timeline
|
||||
<Badge
|
||||
v-if="
|
||||
ticket.expand.timeline_via_ticket &&
|
||||
ticket.expand.timeline_via_ticket.length > 0
|
||||
"
|
||||
variant="outline"
|
||||
class="ml-2 hidden sm:inline-flex"
|
||||
>
|
||||
{{
|
||||
ticket.expand.timeline_via_ticket
|
||||
? ticket.expand.timeline_via_ticket.length
|
||||
: 0
|
||||
}}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="tasks">
|
||||
Tasks
|
||||
<Badge
|
||||
v-if="
|
||||
ticket.expand.tasks_via_ticket && ticket.expand.tasks_via_ticket.length > 0
|
||||
"
|
||||
variant="outline"
|
||||
class="ml-2 hidden sm:inline-flex"
|
||||
>
|
||||
{{ ticket.expand.tasks_via_ticket ? ticket.expand.tasks_via_ticket.length : 0 }}
|
||||
<StatusIcon :status="taskStatus" class="size-6" />
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="comments">
|
||||
Comments
|
||||
<Badge
|
||||
v-if="
|
||||
ticket.expand.comments_via_ticket &&
|
||||
ticket.expand.comments_via_ticket.length > 0
|
||||
"
|
||||
variant="outline"
|
||||
class="ml-2 hidden sm:inline-flex"
|
||||
>
|
||||
{{
|
||||
ticket.expand.comments_via_ticket
|
||||
? ticket.expand.comments_via_ticket.length
|
||||
: 0
|
||||
}}
|
||||
</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TicketTab value="timeline">
|
||||
<TicketTimeline :ticket="ticket" :timeline="ticket.expand.timeline_via_ticket" />
|
||||
</TicketTab>
|
||||
<TicketTab value="tasks">
|
||||
<TicketTasks :ticket="ticket" :tasks="ticket.expand.tasks_via_ticket" />
|
||||
</TicketTab>
|
||||
<TicketTab value="comments">
|
||||
<TicketComments :ticket="ticket" :comments="ticket.expand.comments_via_ticket" />
|
||||
</TicketTab>
|
||||
</Tabs>
|
||||
<Separator class="xl:hidden" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-4 xl:w-96 xl:flex-initial">
|
||||
<div>
|
||||
<div class="flex h-10 flex-row items-center justify-between text-muted-foreground">
|
||||
<span class="text-sm font-semibold"> Details </span>
|
||||
@@ -196,10 +207,10 @@ const updateDescription = (value: string) => (message.value = value)
|
||||
<Separator />
|
||||
<TicketFiles :ticket="ticket" :files="ticket.expand.files_via_ticket" />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</ColumnBodyContainer>
|
||||
</ColumnBody>
|
||||
<Separator />
|
||||
<TicketCloseBar :ticket="ticket" />
|
||||
</div>
|
||||
<TicketCloseBar :ticket="ticket" class="shrink-0" />
|
||||
</template>
|
||||
</TanView>
|
||||
</template>
|
||||
|
||||
@@ -38,13 +38,13 @@ const updateName = (value: string) => {
|
||||
<DynamicInput :modelValue="ticket.name" @update:modelValue="updateName" class="-mx-1" />
|
||||
</span>
|
||||
|
||||
<div class="flex flex-row space-x-2 px-1 text-xs">
|
||||
<div class="flex items-center gap-1 text-muted-foreground">
|
||||
<div class="flex flex-col items-stretch gap-1 text-xs text-muted-foreground md:h-4 md:flex-row">
|
||||
<div>
|
||||
Created:
|
||||
{{ format(new Date(ticket.created), 'PPpp') }}
|
||||
</div>
|
||||
<Separator orientation="vertical" />
|
||||
<div class="flex items-center gap-1 text-muted-foreground">
|
||||
<Separator orientation="vertical" class="hidden md:block" />
|
||||
<div>
|
||||
Updated:
|
||||
{{ format(new Date(ticket.updated), 'PPpp') }}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||
import TicketListList from '@/components/ticket/TicketListList.vue'
|
||||
import TicketNewDialog from '@/components/ticket/TicketNewDialog.vue'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
@@ -14,7 +15,6 @@ import {
|
||||
PaginationNext,
|
||||
PaginationPrev
|
||||
} from '@/components/ui/pagination'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
|
||||
@@ -106,7 +106,7 @@ watch(
|
||||
if (!route.params.id && ticketItems.value && ticketItems.value.items.length > 0) {
|
||||
router.push({
|
||||
name: 'tickets',
|
||||
params: { type: props.selectedType.id, id: ticketItems.value.items[0].id }
|
||||
params: { type: props.selectedType.id }
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -121,90 +121,81 @@ watch([tab, props.selectedType, page, perPage], () => refetch())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-screen flex-col">
|
||||
<div class="flex items-center bg-background px-4 py-2">
|
||||
<h1 class="text-xl font-bold">
|
||||
{{ selectedType?.plural }}
|
||||
</h1>
|
||||
<div class="ml-auto">
|
||||
<TicketNewDialog :selectedType="selectedType" />
|
||||
</div>
|
||||
<ColumnHeader :title="selectedType?.plural">
|
||||
<div class="ml-auto">
|
||||
<TicketNewDialog :selectedType="selectedType" />
|
||||
</div>
|
||||
</ColumnHeader>
|
||||
<Tabs v-model="tab" class="flex flex-1 flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-between px-2 pt-2">
|
||||
<TabsList>
|
||||
<TabsTrigger value="all">All</TabsTrigger>
|
||||
<TabsTrigger value="open">Open</TabsTrigger>
|
||||
<TabsTrigger value="closed">Closed</TabsTrigger>
|
||||
</TabsList>
|
||||
<!-- Button variant="outline" size="sm" class="h-7 gap-1 rounded-md px-3">
|
||||
<ListFilter class="h-3.5 w-3.5" />
|
||||
<span class="sr-only sm:not-sr-only">Filter</span>
|
||||
</Button-->
|
||||
</div>
|
||||
<div class="p-2">
|
||||
<form>
|
||||
<div class="relative flex flex-row items-center">
|
||||
<Input v-model="searchValue" placeholder="Search" @keydown.enter.prevent class="pl-8" />
|
||||
<span class="absolute inset-y-0 start-0 flex items-center justify-center px-2">
|
||||
<Search class="size-4 text-muted-foreground" />
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<Separator />
|
||||
<Tabs v-model="tab" class="flex flex-1 flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-between px-4 pt-2">
|
||||
<TabsList>
|
||||
<TabsTrigger value="all">All</TabsTrigger>
|
||||
<TabsTrigger value="open">Open</TabsTrigger>
|
||||
<TabsTrigger value="closed">Closed</TabsTrigger>
|
||||
</TabsList>
|
||||
<!-- Button variant="outline" size="sm" class="h-7 gap-1 rounded-md px-3">
|
||||
<ListFilter class="h-3.5 w-3.5" />
|
||||
<span class="sr-only sm:not-sr-only">Filter</span>
|
||||
</Button-->
|
||||
</div>
|
||||
<div class="px-4 py-2">
|
||||
<form>
|
||||
<div class="relative flex flex-row items-center">
|
||||
<Input v-model="searchValue" placeholder="Search" @keydown.enter.prevent class="pl-8" />
|
||||
<span class="absolute inset-y-0 start-0 flex items-center justify-center px-2">
|
||||
<Search class="size-4 text-muted-foreground" />
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<Separator />
|
||||
<div v-if="isPending" class="flex h-full w-full items-center justify-center">
|
||||
<LoaderCircle class="h-16 w-16 animate-spin text-primary" />
|
||||
</div>
|
||||
<Alert v-else-if="isError" variant="destructive" class="mb-4 h-screen w-screen">
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{{ error }}</AlertDescription>
|
||||
</Alert>
|
||||
<ScrollArea v-else-if="ticketItems" class="flex-1">
|
||||
<TicketListList :tickets="ticketItems.items" />
|
||||
</ScrollArea>
|
||||
<Separator />
|
||||
<div class="my-2 flex items-center justify-center">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ ticketItems ? ticketItems.items.length : '?' }} of
|
||||
{{ ticketItems ? ticketItems.totalItems : '?' }} tickets
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-4 flex items-center justify-center">
|
||||
<Pagination
|
||||
v-slot="{ page }"
|
||||
:total="ticketItems ? ticketItems.totalItems : 0"
|
||||
:itemsPerPage="perPage"
|
||||
:sibling-count="0"
|
||||
:default-page="1"
|
||||
@update:page="page = $event"
|
||||
>
|
||||
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
|
||||
<PaginationFirst />
|
||||
<PaginationPrev />
|
||||
<div v-if="isPending" class="flex h-full w-full items-center justify-center">
|
||||
<LoaderCircle class="h-16 w-16 animate-spin text-primary" />
|
||||
</div>
|
||||
<Alert v-else-if="isError" variant="destructive" class="mb-2 h-screen w-screen">
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{{ error }}</AlertDescription>
|
||||
</Alert>
|
||||
<div v-else-if="ticketItems" class="flex-1 overflow-y-auto overflow-x-hidden">
|
||||
<TicketListList :tickets="ticketItems.items" />
|
||||
</div>
|
||||
<Separator />
|
||||
<div class="my-2 flex items-center justify-center">
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ ticketItems ? ticketItems.items.length : '?' }} of
|
||||
{{ ticketItems ? ticketItems.totalItems : '?' }} tickets
|
||||
</span>
|
||||
</div>
|
||||
<div class="mb-2 flex items-center justify-center">
|
||||
<Pagination
|
||||
v-slot="{ page }"
|
||||
:total="ticketItems ? ticketItems.totalItems : 0"
|
||||
:itemsPerPage="perPage"
|
||||
:sibling-count="0"
|
||||
:default-page="1"
|
||||
@update:page="page = $event"
|
||||
>
|
||||
<PaginationList v-slot="{ items }" class="flex items-center gap-1">
|
||||
<PaginationFirst />
|
||||
<PaginationPrev />
|
||||
|
||||
<template v-for="(item, index) in items">
|
||||
<PaginationListItem
|
||||
v-if="item.type === 'page'"
|
||||
:key="index"
|
||||
:value="item.value"
|
||||
as-child
|
||||
>
|
||||
<Button
|
||||
class="h-10 w-10 p-0"
|
||||
:variant="item.value === page ? 'default' : 'outline'"
|
||||
>
|
||||
{{ item.value }}
|
||||
</Button>
|
||||
</PaginationListItem>
|
||||
<PaginationEllipsis v-else :key="item.type" :index="index" />
|
||||
</template>
|
||||
<PaginationNext />
|
||||
<PaginationLast />
|
||||
</PaginationList>
|
||||
</Pagination>
|
||||
</div>
|
||||
</Tabs>
|
||||
</div>
|
||||
<template v-for="(item, index) in items">
|
||||
<PaginationListItem
|
||||
v-if="item.type === 'page'"
|
||||
:key="index"
|
||||
:value="item.value"
|
||||
as-child
|
||||
>
|
||||
<Button class="h-10 w-10 p-0" :variant="item.value === page ? 'default' : 'outline'">
|
||||
{{ item.value }}
|
||||
</Button>
|
||||
</PaginationListItem>
|
||||
<PaginationEllipsis v-else :key="item.type" :index="index" />
|
||||
</template>
|
||||
<PaginationNext />
|
||||
<PaginationLast />
|
||||
</PaginationList>
|
||||
</Pagination>
|
||||
</div>
|
||||
</Tabs>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import ResourceListElement from '@/components/common/ResourceListElement.vue'
|
||||
import ResourceListElement from '@/components/layout/ResourceListElement.vue'
|
||||
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
@@ -13,7 +13,7 @@ defineProps<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-2 flex w-full flex-1 flex-col gap-2 p-4 pt-0">
|
||||
<div class="mt-2 flex w-full flex-1 flex-col gap-2 p-2 pt-0">
|
||||
<ResourceListElement
|
||||
v-for="item of tickets"
|
||||
:key="item.id"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { TabsContent } from '@/components/ui/tabs'
|
||||
|
||||
@@ -12,10 +11,8 @@ defineProps<{
|
||||
<TabsContent :value="value" class="flex-1 overflow-hidden">
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<Separator class="mt-2" />
|
||||
<ScrollArea class="flex-1">
|
||||
<slot />
|
||||
<div class="h-4" />
|
||||
</ScrollArea>
|
||||
<slot />
|
||||
<div class="h-4" />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
||||
import PanelListElement from '@/components/common/PanelListElement.vue'
|
||||
import PanelListElement from '@/components/layout/PanelListElement.vue'
|
||||
import TicketPanel from '@/components/ticket/TicketPanel.vue'
|
||||
import LinkAddDialog from '@/components/ticket/link/LinkAddDialog.vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -28,7 +28,12 @@ const dialogOpen = ref(false)
|
||||
>
|
||||
No links added yet.
|
||||
</div>
|
||||
<PanelListElement v-for="link in links" :key="link.id" :title="link.url" class="pr-1">
|
||||
<PanelListElement
|
||||
v-for="link in links"
|
||||
:key="link.id"
|
||||
:title="link.url"
|
||||
class="flex-row items-center pr-1"
|
||||
>
|
||||
<a :href="link.url" target="_blank" class="flex flex-1 items-center overflow-hidden">
|
||||
<span class="mr-2 text-blue-500 underline">
|
||||
{{ link.name }}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import DeleteDialog from '@/components/common/DeleteDialog.vue'
|
||||
import PanelListElement from '@/components/common/PanelListElement.vue'
|
||||
import UserSelect from '@/components/common/UserSelect.vue'
|
||||
import DynamicInput from '@/components/input/DynamicInput.vue'
|
||||
import PanelListElement from '@/components/layout/PanelListElement.vue'
|
||||
import TaskAddDialog from '@/components/ticket/task/TaskAddDialog.vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card } from '@/components/ui/card'
|
||||
@@ -67,12 +67,14 @@ const updateTaskName = (id: string, name: string) => updateTaskNameMutation.muta
|
||||
</Card>
|
||||
<Card v-else>
|
||||
<PanelListElement v-for="task in tasks" :key="task.id" class="pr-1">
|
||||
<Checkbox :checked="!task.open" class="mr-2" @click="check(task)" />
|
||||
<DynamicInput
|
||||
:modelValue="task.name"
|
||||
@update:modelValue="updateTaskName(task.id, $event)"
|
||||
class="mr-2 flex-1"
|
||||
/>
|
||||
<div class="flex flex-row items-center">
|
||||
<Checkbox :checked="!task.open" class="mr-2" @click="check(task)" />
|
||||
<DynamicInput
|
||||
:modelValue="task.name"
|
||||
@update:modelValue="updateTaskName(task.id, $event)"
|
||||
class="mr-2 flex-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-auto flex items-center">
|
||||
<UserSelect v-if="!task.expand.owner" @update:modelValue="update(task.id, $event)">
|
||||
<Button variant="outline" role="combobox" class="h-8">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import PanelListElement from '@/components/common/PanelListElement.vue'
|
||||
import DynamicMDEditor from '@/components/input/DynamicMDEditor.vue'
|
||||
import PanelListElement from '@/components/layout/PanelListElement.vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AccordionRoot,
|
||||
type AccordionRootEmits,
|
||||
type AccordionRootProps,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
|
||||
const props = defineProps<AccordionRootProps>()
|
||||
const emits = defineEmits<AccordionRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</AccordionRoot>
|
||||
</template>
|
||||
@@ -1,25 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { AccordionContent, type AccordionContentProps } from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<AccordionContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionContent
|
||||
v-bind="delegatedProps"
|
||||
class="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
>
|
||||
<div :class="cn('pb-4 pt-0', props.class)">
|
||||
<slot />
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</template>
|
||||
@@ -1,22 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { AccordionItem, type AccordionItemProps, useForwardProps } from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<AccordionItemProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionItem v-bind="forwardedProps" :class="cn('border-b', props.class)">
|
||||
<slot />
|
||||
</AccordionItem>
|
||||
</template>
|
||||
@@ -1,33 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { AccordionHeader, AccordionTrigger, type AccordionTriggerProps } from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<AccordionTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AccordionHeader class="flex">
|
||||
<AccordionTrigger
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<!-- slot name="icon">
|
||||
<ChevronDown class="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</slot-->
|
||||
</AccordionTrigger>
|
||||
</AccordionHeader>
|
||||
</template>
|
||||
@@ -1,4 +0,0 @@
|
||||
export { default as Accordion } from './Accordion.vue'
|
||||
export { default as AccordionContent } from './AccordionContent.vue'
|
||||
export { default as AccordionItem } from './AccordionItem.vue'
|
||||
export { default as AccordionTrigger } from './AccordionTrigger.vue'
|
||||
@@ -1,25 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { type AvatarVariants, avatarVariant } from '.'
|
||||
import { AvatarRoot } from 'radix-vue'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
class?: HTMLAttributes['class']
|
||||
size?: AvatarVariants['size']
|
||||
shape?: AvatarVariants['shape']
|
||||
}>(),
|
||||
{
|
||||
size: 'sm',
|
||||
shape: 'circle'
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarRoot :class="cn(avatarVariant({ size, shape }), props.class)">
|
||||
<slot />
|
||||
</AvatarRoot>
|
||||
</template>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { AvatarFallback, type AvatarFallbackProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<AvatarFallbackProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarFallback v-bind="props">
|
||||
<slot />
|
||||
</AvatarFallback>
|
||||
</template>
|
||||
@@ -1,9 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { AvatarImage, type AvatarImageProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<AvatarImageProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AvatarImage v-bind="props" class="h-full w-full object-cover" />
|
||||
</template>
|
||||
@@ -1,24 +0,0 @@
|
||||
import { type VariantProps, cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Avatar } from './Avatar.vue'
|
||||
export { default as AvatarImage } from './AvatarImage.vue'
|
||||
export { default as AvatarFallback } from './AvatarFallback.vue'
|
||||
|
||||
export const avatarVariant = cva(
|
||||
'inline-flex items-center justify-center font-normal text-foreground select-none shrink-0 bg-secondary overflow-hidden',
|
||||
{
|
||||
variants: {
|
||||
size: {
|
||||
sm: 'h-10 w-10 text-xs',
|
||||
base: 'h-16 w-16 text-2xl',
|
||||
lg: 'h-32 w-32 text-5xl'
|
||||
},
|
||||
shape: {
|
||||
circle: 'rounded-full',
|
||||
square: 'rounded-md'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export type AvatarVariants = VariantProps<typeof avatarVariant>
|
||||
@@ -1,69 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
CalendarCell,
|
||||
CalendarCellTrigger,
|
||||
CalendarGrid,
|
||||
CalendarGridBody,
|
||||
CalendarGridHead,
|
||||
CalendarGridRow,
|
||||
CalendarHeadCell,
|
||||
CalendarHeader,
|
||||
CalendarHeading,
|
||||
CalendarNextButton,
|
||||
CalendarPrevButton
|
||||
} from '.'
|
||||
import {
|
||||
CalendarRoot,
|
||||
type CalendarRootEmits,
|
||||
type CalendarRootProps,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const emits = defineEmits<CalendarRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarRoot v-slot="{ grid, weekDays }" :class="cn('p-3', props.class)" v-bind="forwarded">
|
||||
<CalendarHeader>
|
||||
<CalendarPrevButton />
|
||||
<CalendarHeading />
|
||||
<CalendarNextButton />
|
||||
</CalendarHeader>
|
||||
|
||||
<div class="mt-4 flex flex-col gap-y-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
|
||||
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
|
||||
<CalendarGridHead>
|
||||
<CalendarGridRow>
|
||||
<CalendarHeadCell v-for="day in weekDays" :key="day">
|
||||
{{ day }}
|
||||
</CalendarHeadCell>
|
||||
</CalendarGridRow>
|
||||
</CalendarGridHead>
|
||||
<CalendarGridBody>
|
||||
<CalendarGridRow
|
||||
v-for="(weekDates, index) in month.rows"
|
||||
:key="`weekDate-${index}`"
|
||||
class="mt-2 w-full"
|
||||
>
|
||||
<CalendarCell v-for="weekDate in weekDates" :key="weekDate.toString()" :date="weekDate">
|
||||
<CalendarCellTrigger :day="weekDate" :month="month.value" />
|
||||
</CalendarCell>
|
||||
</CalendarGridRow>
|
||||
</CalendarGridBody>
|
||||
</CalendarGrid>
|
||||
</div>
|
||||
</CalendarRoot>
|
||||
</template>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarCell
|
||||
:class="
|
||||
cn(
|
||||
'relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-month])]:bg-accent/50',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</CalendarCell>
|
||||
</template>
|
||||
@@ -1,42 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
|
||||
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarCellTrigger
|
||||
:class="
|
||||
cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'h-9 w-9 p-0 font-normal',
|
||||
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
|
||||
// Selected
|
||||
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
|
||||
// Disabled
|
||||
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
|
||||
// Unavailable
|
||||
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
|
||||
// Outside months
|
||||
'data-[outside-month]:pointer-events-none data-[outside-month]:text-muted-foreground data-[outside-month]:opacity-50 [&[data-outside-month][data-selected]]:bg-accent/50 [&[data-outside-month][data-selected]]:text-muted-foreground [&[data-outside-month][data-selected]]:opacity-30',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</CalendarCellTrigger>
|
||||
</template>
|
||||
@@ -1,25 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { CalendarGrid, type CalendarGridProps, useForwardProps } from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarGrid
|
||||
:class="cn('w-full border-collapse space-y-1', props.class)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</CalendarGrid>
|
||||
</template>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { CalendarGridBody, type CalendarGridBodyProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<CalendarGridBodyProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarGridBody v-bind="props">
|
||||
<slot />
|
||||
</CalendarGridBody>
|
||||
</template>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { CalendarGridHead, type CalendarGridHeadProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<CalendarGridHeadProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarGridHead v-bind="props">
|
||||
<slot />
|
||||
</CalendarGridHead>
|
||||
</template>
|
||||
@@ -1,22 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { CalendarGridRow, type CalendarGridRowProps, useForwardProps } from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarGridRow :class="cn('flex', props.class)" v-bind="forwardedProps">
|
||||
<slot />
|
||||
</CalendarGridRow>
|
||||
</template>
|
||||
@@ -1,25 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { CalendarHeadCell, type CalendarHeadCellProps, useForwardProps } from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarHeadCell
|
||||
:class="cn('w-9 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</CalendarHeadCell>
|
||||
</template>
|
||||
@@ -1,25 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { CalendarHeader, type CalendarHeaderProps, useForwardProps } from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarHeader
|
||||
:class="cn('relative flex w-full items-center justify-between pt-1', props.class)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot />
|
||||
</CalendarHeader>
|
||||
</template>
|
||||
@@ -1,28 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { CalendarHeading, type CalendarHeadingProps, useForwardProps } from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarHeadingProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarHeading
|
||||
v-slot="{ headingValue }"
|
||||
:class="cn('text-sm font-medium', props.class)"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot :heading-value>
|
||||
{{ headingValue }}
|
||||
</slot>
|
||||
</CalendarHeading>
|
||||
</template>
|
||||
@@ -1,37 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
|
||||
import { CalendarNext, type CalendarNextProps, useForwardProps } from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarNextProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarNext
|
||||
:class="
|
||||
cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot>
|
||||
<ChevronRight class="h-4 w-4" />
|
||||
</slot>
|
||||
</CalendarNext>
|
||||
</template>
|
||||
@@ -1,37 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
|
||||
import { ChevronLeft } from 'lucide-vue-next'
|
||||
|
||||
import { CalendarPrev, type CalendarPrevProps, useForwardProps } from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<CalendarPrevProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CalendarPrev
|
||||
:class="
|
||||
cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
v-bind="forwardedProps"
|
||||
>
|
||||
<slot>
|
||||
<ChevronLeft class="h-4 w-4" />
|
||||
</slot>
|
||||
</CalendarPrev>
|
||||
</template>
|
||||
@@ -1,12 +0,0 @@
|
||||
export { default as Calendar } from './Calendar.vue'
|
||||
export { default as CalendarCell } from './CalendarCell.vue'
|
||||
export { default as CalendarCellTrigger } from './CalendarCellTrigger.vue'
|
||||
export { default as CalendarGrid } from './CalendarGrid.vue'
|
||||
export { default as CalendarGridBody } from './CalendarGridBody.vue'
|
||||
export { default as CalendarGridHead } from './CalendarGridHead.vue'
|
||||
export { default as CalendarGridRow } from './CalendarGridRow.vue'
|
||||
export { default as CalendarHeadCell } from './CalendarHeadCell.vue'
|
||||
export { default as CalendarHeader } from './CalendarHeader.vue'
|
||||
export { default as CalendarHeading } from './CalendarHeading.vue'
|
||||
export { default as CalendarNextButton } from './CalendarNextButton.vue'
|
||||
export { default as CalendarPrevButton } from './CalendarPrevButton.vue'
|
||||
@@ -1,15 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { CollapsibleRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
import type { CollapsibleRootEmits, CollapsibleRootProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<CollapsibleRootProps>()
|
||||
const emits = defineEmits<CollapsibleRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapsibleRoot v-slot="{ open }" v-bind="forwarded">
|
||||
<slot :open="open" />
|
||||
</CollapsibleRoot>
|
||||
</template>
|
||||
@@ -1,14 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { CollapsibleContent, type CollapsibleContentProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<CollapsibleContentProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapsibleContent
|
||||
v-bind="props"
|
||||
class="overflow-hidden transition-all data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down"
|
||||
>
|
||||
<slot />
|
||||
</CollapsibleContent>
|
||||
</template>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { CollapsibleTrigger, type CollapsibleTriggerProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<CollapsibleTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapsibleTrigger v-bind="props">
|
||||
<slot />
|
||||
</CollapsibleTrigger>
|
||||
</template>
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as Collapsible } from './Collapsible.vue'
|
||||
export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue'
|
||||
export { default as CollapsibleContent } from './CollapsibleContent.vue'
|
||||
@@ -1,43 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { GripVertical } from 'lucide-vue-next'
|
||||
|
||||
import {
|
||||
SplitterResizeHandle,
|
||||
type SplitterResizeHandleEmits,
|
||||
type SplitterResizeHandleProps,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<
|
||||
SplitterResizeHandleProps & { class?: HTMLAttributes['class']; withHandle?: boolean }
|
||||
>()
|
||||
const emits = defineEmits<SplitterResizeHandleEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SplitterResizeHandle
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 [&[data-orientation=vertical]>div]:rotate-90 [&[data-orientation=vertical]]:h-px [&[data-orientation=vertical]]:w-full [&[data-orientation=vertical]]:after:left-0 [&[data-orientation=vertical]]:after:h-1 [&[data-orientation=vertical]]:after:w-full [&[data-orientation=vertical]]:after:-translate-y-1/2 [&[data-orientation=vertical]]:after:translate-x-0',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<template v-if="props.withHandle">
|
||||
<div class="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
||||
<GripVertical class="h-2.5 w-2.5" />
|
||||
</div>
|
||||
</template>
|
||||
</SplitterResizeHandle>
|
||||
</template>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
SplitterGroup,
|
||||
type SplitterGroupEmits,
|
||||
type SplitterGroupProps,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SplitterGroupProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<SplitterGroupEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SplitterGroup
|
||||
v-bind="forwarded"
|
||||
:class="cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</SplitterGroup>
|
||||
</template>
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as ResizablePanelGroup } from './ResizablePanelGroup.vue'
|
||||
export { default as ResizableHandle } from './ResizableHandle.vue'
|
||||
export { SplitterPanel as ResizablePanel } from 'radix-vue'
|
||||
@@ -1,30 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import ScrollBar from './ScrollBar.vue'
|
||||
import {
|
||||
ScrollAreaCorner,
|
||||
ScrollAreaRoot,
|
||||
type ScrollAreaRootProps,
|
||||
ScrollAreaViewport
|
||||
} from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollAreaRoot v-bind="delegatedProps" :class="cn('relative overflow-hidden', props.class)">
|
||||
<ScrollAreaViewport class="h-full w-full rounded-[inherit]">
|
||||
<slot />
|
||||
</ScrollAreaViewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaCorner />
|
||||
</ScrollAreaRoot>
|
||||
</template>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ScrollAreaScrollbar, type ScrollAreaScrollbarProps, ScrollAreaThumb } from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes['class'] }>(),
|
||||
{
|
||||
orientation: 'vertical'
|
||||
}
|
||||
)
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollAreaScrollbar
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex touch-none select-none transition-colors',
|
||||
orientation === 'vertical' && 'h-full w-2.5 border-l border-l-transparent p-px',
|
||||
orientation === 'horizontal' && 'h-2.5 flex-col border-t border-t-transparent p-px',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<ScrollAreaThumb class="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaScrollbar>
|
||||
</template>
|
||||
@@ -1,2 +0,0 @@
|
||||
export { default as ScrollArea } from './ScrollArea.vue'
|
||||
export { default as ScrollBar } from './ScrollBar.vue'
|
||||
@@ -1,44 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
SwitchRoot,
|
||||
type SwitchRootEmits,
|
||||
type SwitchRootProps,
|
||||
SwitchThumb,
|
||||
useForwardPropsEmits
|
||||
} from 'radix-vue'
|
||||
import { type HTMLAttributes, computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<SwitchRootProps & { class?: HTMLAttributes['class'] }>()
|
||||
|
||||
const emits = defineEmits<SwitchRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchRoot
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input',
|
||||
props.class
|
||||
)
|
||||
"
|
||||
>
|
||||
<SwitchThumb
|
||||
:class="
|
||||
cn(
|
||||
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</SwitchRoot>
|
||||
</template>
|
||||
@@ -1 +0,0 @@
|
||||
export { default as Switch } from './Switch.vue'
|
||||
@@ -3,10 +3,11 @@ import OpenTasks from '@/components/dashboard/OpenTasks.vue'
|
||||
import OpenTickets from '@/components/dashboard/OpenTickets.vue'
|
||||
import TicketOverTime from '@/components/dashboard/TicketOverTime.vue'
|
||||
import TicketTypes from '@/components/dashboard/TicketTypes.vue'
|
||||
import ColumnBody from '@/components/layout/ColumnBody.vue'
|
||||
import ColumnBodyContainer from '@/components/layout/ColumnBodyContainer.vue'
|
||||
import ColumnHeader from '@/components/layout/ColumnHeader.vue'
|
||||
import TwoColumn from '@/components/layout/TwoColumn.vue'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
|
||||
import { ExternalLink } from 'lucide-vue-next'
|
||||
|
||||
@@ -46,96 +47,91 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<TwoColumn>
|
||||
<div class="flex h-screen flex-1 flex-col">
|
||||
<div class="flex h-14 min-h-14 items-center bg-background px-4 py-2">
|
||||
<h1 class="text-xl font-bold">Dashboard</h1>
|
||||
</div>
|
||||
<Separator class="shrink-0" />
|
||||
<ScrollArea>
|
||||
<div
|
||||
class="m-auto grid max-w-7xl grid-cols-1 grid-rows-[100px_100px_100px_100px] gap-4 p-4 md:grid-cols-2 md:grid-rows-[100px_100px] xl:grid-cols-4 xl:grid-rows-[100px]"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ count('tasks') }}</CardTitle>
|
||||
<CardDescription>Tasks</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ count('tickets') }}</CardTitle>
|
||||
<CardDescription>Tickets</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ count('users') }}</CardTitle>
|
||||
<CardDescription>Users</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle></CardTitle>
|
||||
<CardDescription></CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle> Catalyst</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-1 flex-col gap-1">
|
||||
<a
|
||||
href="https://catalyst-soar.com/docs/category/catalyst-handbook"
|
||||
target="_blank"
|
||||
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
||||
>
|
||||
Open Catalyst Handbook
|
||||
<ExternalLink class="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
<a
|
||||
href="/_/"
|
||||
target="_blank"
|
||||
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
||||
>
|
||||
Open Admin Interface
|
||||
<ExternalLink class="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle> Tickets by Type</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TicketTypes />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="xl:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Tickets Per Week</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TicketOverTime />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="xl:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Your Open Tickets</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<OpenTickets />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="xl:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Your Open Tasks</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<OpenTasks />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
<ColumnHeader title="Dashboard" />
|
||||
<ColumnBody>
|
||||
<ColumnBodyContainer
|
||||
class="grid grid-cols-1 grid-rows-[100px_100px_100px_100px] md:grid-cols-2 md:grid-rows-[100px_100px] xl:grid-cols-4 xl:grid-rows-[100px]"
|
||||
>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ count('tasks') }}</CardTitle>
|
||||
<CardDescription>Tasks</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ count('tickets') }}</CardTitle>
|
||||
<CardDescription>Tickets</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ count('users') }}</CardTitle>
|
||||
<CardDescription>Users</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{{ count('reactions') }}</CardTitle>
|
||||
<CardDescription>Reactions</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle> Catalyst</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="flex flex-1 flex-col gap-1">
|
||||
<a
|
||||
href="https://catalyst.security-brewery.com/docs/category/catalyst-handbook"
|
||||
target="_blank"
|
||||
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
||||
>
|
||||
Open Catalyst Handbook
|
||||
<ExternalLink class="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
<a
|
||||
href="/_/"
|
||||
target="_blank"
|
||||
class="flex items-center rounded border p-2 text-blue-500 hover:bg-accent"
|
||||
>
|
||||
Open Admin Interface
|
||||
<ExternalLink class="ml-2 h-4 w-4" />
|
||||
</a>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle> Tickets by Type</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TicketTypes />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="xl:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Tickets Per Week</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<TicketOverTime />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="xl:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Your Open Tickets</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<OpenTickets />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card class="xl:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Your Open Tasks</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<OpenTasks />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</ColumnBodyContainer>
|
||||
</ColumnBody>
|
||||
</TwoColumn>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts" xmlns="http://www.w3.org/1999/html">
|
||||
<script setup lang="ts">
|
||||
import ColumnBody from '@/components/layout/ColumnBody.vue'
|
||||
import ThreeColumn from '@/components/layout/ThreeColumn.vue'
|
||||
import ReactionDisplay from '@/components/reaction/ReactionDisplay.vue'
|
||||
import ReactionList from '@/components/reaction/ReactionList.vue'
|
||||
@@ -22,14 +23,14 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ThreeColumn>
|
||||
<ThreeColumn :show-details="!!id">
|
||||
<template #list>
|
||||
<ReactionList />
|
||||
</template>
|
||||
<template #single>
|
||||
<div v-if="!id" class="flex h-full w-full items-center justify-center text-lg text-gray-500">
|
||||
<ColumnBody v-if="!id" class="items-center justify-center text-lg text-gray-500">
|
||||
No reaction selected
|
||||
</div>
|
||||
</ColumnBody>
|
||||
<ReactionNew v-else-if="id === 'new'" key="new" />
|
||||
<ReactionDisplay v-else :key="id" :id="id" />
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import TanView from '@/components/TanView.vue'
|
||||
import ColumnBody from '@/components/layout/ColumnBody.vue'
|
||||
import ThreeColumn from '@/components/layout/ThreeColumn.vue'
|
||||
import TicketDisplay from '@/components/ticket/TicketDisplay.vue'
|
||||
import TicketList from '@/components/ticket/TicketList.vue'
|
||||
@@ -41,20 +42,17 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ThreeColumn>
|
||||
<ThreeColumn :show-details="!!id">
|
||||
<template #list>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="selectedType">
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||
<TicketList v-if="selectedType" :key="selectedType.id" :selectedType="selectedType" />
|
||||
</TanView>
|
||||
</template>
|
||||
<template #single>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error" :value="selectedType">
|
||||
<div
|
||||
v-if="!id"
|
||||
class="flex h-full w-full items-center justify-center text-lg text-gray-500"
|
||||
>
|
||||
<TanView :isError="isError" :isPending="isPending" :error="error">
|
||||
<ColumnBody v-if="!id" class="items-center justify-center text-lg text-gray-500">
|
||||
No ticket selected
|
||||
</div>
|
||||
</ColumnBody>
|
||||
<TicketDisplay v-else-if="selectedType" :key="id" :selectedType="selectedType" />
|
||||
</TanView>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user