mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-01 13:45:44 +00:00
Compare commits
5 Commits
experiment
...
better_fil
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02cf02b5dd | ||
|
|
3e2a08a42a | ||
|
|
da17b4aa36 | ||
|
|
4142ae0afd | ||
|
|
5d857a5112 |
433
web/package-lock.json
generated
433
web/package-lock.json
generated
@@ -44,6 +44,7 @@
|
||||
"autoprefixer": "^10.4.14",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"favicon-fetch": "^1.0.0",
|
||||
"formik": "^2.2.9",
|
||||
@@ -9313,6 +9314,438 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.0.0.tgz",
|
||||
"integrity": "sha512-gDzVf0a09TvoJ5jnuPvygTB77+XdOSwEmJ88L6XPFPlv7T3RxbP9jgenfylrAMD0+Le1aO0nVjQUzl2g+vjz5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "1.0.5",
|
||||
"@radix-ui/react-primitive": "1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz",
|
||||
"integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz",
|
||||
"integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz",
|
||||
"integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz",
|
||||
"integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.0.5",
|
||||
"@radix-ui/react-focus-guards": "1.0.1",
|
||||
"@radix-ui/react-focus-scope": "1.0.4",
|
||||
"@radix-ui/react-id": "1.0.1",
|
||||
"@radix-ui/react-portal": "1.0.4",
|
||||
"@radix-ui/react-presence": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-slot": "1.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.1",
|
||||
"aria-hidden": "^1.1.1",
|
||||
"react-remove-scroll": "2.5.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz",
|
||||
"integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1",
|
||||
"@radix-ui/react-use-escape-keydown": "1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
|
||||
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz",
|
||||
"integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-focus-guards": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz",
|
||||
"integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz",
|
||||
"integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
|
||||
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz",
|
||||
"integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-id/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
|
||||
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz",
|
||||
"integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-primitive": "1.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz",
|
||||
"integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-use-layout-effect": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-presence/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz",
|
||||
"integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz",
|
||||
"integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
|
||||
"integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-compose-refs": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz",
|
||||
"integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/@radix-ui/react-use-controllable-state/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz",
|
||||
"integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/cmdk/node_modules/react-remove-scroll": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz",
|
||||
"integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-remove-scroll-bar": "^2.3.3",
|
||||
"react-style-singleton": "^2.2.1",
|
||||
"tslib": "^2.1.0",
|
||||
"use-callback-ref": "^1.3.0",
|
||||
"use-sidecar": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"autoprefixer": "^10.4.14",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"favicon-fetch": "^1.0.0",
|
||||
"formik": "^2.2.9",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ArrayHelpers, FieldArray, Form, Formik } from "formik";
|
||||
import { Form, Formik } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import {
|
||||
@@ -10,13 +10,14 @@ import {
|
||||
} from "./lib";
|
||||
import { ConnectorStatus, DocumentSet, UserGroup, UserRole } from "@/lib/types";
|
||||
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
import { IsPublicGroupSelector } from "@/components/IsPublicGroupSelector";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { ConnectorMultiSelect } from "@/components/ConnectorMultiSelect";
|
||||
import { NonSelectableConnectors } from "@/components/NonSelectableConnectors";
|
||||
|
||||
interface SetCreationPopupProps {
|
||||
ccPairs: ConnectorStatus<any, any>[];
|
||||
@@ -45,7 +46,7 @@ export const DocumentSetCreationForm = ({
|
||||
}, [existingDocumentSet?.is_public]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="max-w-full mx-auto">
|
||||
<Formik<DocumentSetCreationRequest>
|
||||
initialValues={{
|
||||
name: existingDocumentSet?.name ?? "",
|
||||
@@ -104,243 +105,122 @@ export const DocumentSetCreationForm = ({
|
||||
}}
|
||||
>
|
||||
{(props) => {
|
||||
// Filter visible cc pairs for curator role
|
||||
const visibleCcPairs =
|
||||
user?.role === UserRole.CURATOR
|
||||
? localCcPairs.filter(
|
||||
(ccPair) =>
|
||||
ccPair.access_type === "public" ||
|
||||
(ccPair.groups.length > 0 &&
|
||||
props.values.groups.every((group) =>
|
||||
ccPair.groups.includes(group)
|
||||
))
|
||||
)
|
||||
: localCcPairs;
|
||||
|
||||
// Filter non-visible cc pairs for curator role
|
||||
const nonVisibleCcPairs =
|
||||
user?.role === UserRole.CURATOR
|
||||
? localCcPairs.filter(
|
||||
(ccPair) =>
|
||||
!(ccPair.access_type === "public") &&
|
||||
(ccPair.groups.length === 0 ||
|
||||
!props.values.groups.every((group) =>
|
||||
ccPair.groups.includes(group)
|
||||
))
|
||||
)
|
||||
: [];
|
||||
|
||||
// Deselect filtered out cc pairs
|
||||
if (user?.role === UserRole.CURATOR) {
|
||||
const visibleCcPairIds = visibleCcPairs.map(
|
||||
(ccPair) => ccPair.cc_pair_id
|
||||
);
|
||||
props.values.cc_pair_ids = props.values.cc_pair_ids.filter((id) =>
|
||||
visibleCcPairIds.includes(id)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<TextFormField
|
||||
name="name"
|
||||
label="Name:"
|
||||
placeholder="A name for the document set"
|
||||
disabled={isUpdate}
|
||||
autoCompleteDisabled={true}
|
||||
/>
|
||||
<TextFormField
|
||||
name="description"
|
||||
label="Description:"
|
||||
placeholder="Describe what the document set represents"
|
||||
autoCompleteDisabled={true}
|
||||
optional={true}
|
||||
/>
|
||||
|
||||
{isPaidEnterpriseFeaturesEnabled && (
|
||||
<IsPublicGroupSelector
|
||||
formikProps={props}
|
||||
objectName="document set"
|
||||
<Form className="space-y-6 w-full ">
|
||||
<div className="space-y-4 w-full">
|
||||
<TextFormField
|
||||
name="name"
|
||||
label="Name:"
|
||||
placeholder="A name for the document set"
|
||||
disabled={isUpdate}
|
||||
autoCompleteDisabled={true}
|
||||
/>
|
||||
<TextFormField
|
||||
name="description"
|
||||
label="Description:"
|
||||
placeholder="Describe what the document set represents"
|
||||
autoCompleteDisabled={true}
|
||||
optional={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
{user?.role === UserRole.CURATOR ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<h2 className="mb-1 font-medium text-base">
|
||||
These are the connectors available to{" "}
|
||||
{userGroups && userGroups.length > 1
|
||||
? "the selected group"
|
||||
: "the group you curate"}
|
||||
:
|
||||
</h2>
|
||||
|
||||
<p className="mb-text-sm">
|
||||
All documents indexed by these selected connectors will be
|
||||
a part of this document set.
|
||||
</p>
|
||||
<FieldArray
|
||||
name="cc_pair_ids"
|
||||
render={(arrayHelpers: ArrayHelpers) => {
|
||||
// Filter visible cc pairs
|
||||
const visibleCcPairs = localCcPairs.filter(
|
||||
(ccPair) =>
|
||||
ccPair.access_type === "public" ||
|
||||
(ccPair.groups.length > 0 &&
|
||||
props.values.groups.every((group) =>
|
||||
ccPair.groups.includes(group)
|
||||
))
|
||||
);
|
||||
|
||||
// Deselect filtered out cc pairs
|
||||
const visibleCcPairIds = visibleCcPairs.map(
|
||||
(ccPair) => ccPair.cc_pair_id
|
||||
);
|
||||
props.values.cc_pair_ids =
|
||||
props.values.cc_pair_ids.filter((id) =>
|
||||
visibleCcPairIds.includes(id)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-3 flex gap-2 flex-wrap">
|
||||
{visibleCcPairs.map((ccPair) => {
|
||||
const ind = props.values.cc_pair_ids.indexOf(
|
||||
ccPair.cc_pair_id
|
||||
);
|
||||
const isSelected = ind !== -1;
|
||||
return (
|
||||
<div
|
||||
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
|
||||
className={
|
||||
`
|
||||
px-3
|
||||
py-1
|
||||
rounded-lg
|
||||
border
|
||||
border-border
|
||||
w-fit
|
||||
flex
|
||||
cursor-pointer ` +
|
||||
(isSelected
|
||||
? " bg-background-200"
|
||||
: " hover:bg-accent-background-hovered")
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
arrayHelpers.remove(ind);
|
||||
} else {
|
||||
arrayHelpers.push(ccPair.cc_pair_id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="my-auto">
|
||||
<ConnectorTitle
|
||||
connector={ccPair.connector}
|
||||
ccPairId={ccPair.cc_pair_id}
|
||||
ccPairName={ccPair.name}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FieldArray
|
||||
name="cc_pair_ids"
|
||||
render={() => {
|
||||
// Filter non-visible cc pairs
|
||||
const nonVisibleCcPairs = localCcPairs.filter(
|
||||
(ccPair) =>
|
||||
!(ccPair.access_type === "public") &&
|
||||
(ccPair.groups.length === 0 ||
|
||||
!props.values.groups.every((group) =>
|
||||
ccPair.groups.includes(group)
|
||||
))
|
||||
);
|
||||
|
||||
return nonVisibleCcPairs.length > 0 ? (
|
||||
<>
|
||||
<Separator />
|
||||
<h2 className="mb-1 font-medium text-base">
|
||||
These connectors are not available to the{" "}
|
||||
{userGroups && userGroups.length > 1
|
||||
? `group${
|
||||
props.values.groups.length > 1 ? "s" : ""
|
||||
} you have selected`
|
||||
: "group you curate"}
|
||||
:
|
||||
</h2>
|
||||
<p className="mb-3 text-sm">
|
||||
Only connectors that are directly assigned to the
|
||||
group you are trying to add the document set to
|
||||
will be available.
|
||||
</p>
|
||||
<div className="mb-3 flex gap-2 flex-wrap">
|
||||
{nonVisibleCcPairs.map((ccPair) => (
|
||||
<div
|
||||
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
|
||||
className="px-3 py-1 rounded-lg border border-non-selectable-border w-fit flex cursor-not-allowed"
|
||||
>
|
||||
<div className="my-auto">
|
||||
<ConnectorTitle
|
||||
connector={ccPair.connector}
|
||||
ccPairId={ccPair.cc_pair_id}
|
||||
ccPairName={ccPair.name}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : null;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<h2 className="mb-1 font-medium text-base">
|
||||
Pick your connectors:
|
||||
</h2>
|
||||
<p className="mb-3 text-xs">
|
||||
All documents indexed by the selected connectors will be a
|
||||
part of this document set.
|
||||
</p>
|
||||
<FieldArray
|
||||
name="cc_pair_ids"
|
||||
render={(arrayHelpers: ArrayHelpers) => (
|
||||
<div className="mb-3 flex gap-2 flex-wrap">
|
||||
{ccPairs.map((ccPair) => {
|
||||
const ind = props.values.cc_pair_ids.indexOf(
|
||||
ccPair.cc_pair_id
|
||||
);
|
||||
const isSelected = ind !== -1;
|
||||
return (
|
||||
<div
|
||||
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
|
||||
className={
|
||||
`
|
||||
px-3
|
||||
py-1
|
||||
rounded-lg
|
||||
border
|
||||
border-border
|
||||
w-fit
|
||||
flex
|
||||
cursor-pointer ` +
|
||||
(isSelected
|
||||
? " bg-background-200"
|
||||
: " hover:bg-accent-background-hovered")
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
arrayHelpers.remove(ind);
|
||||
} else {
|
||||
arrayHelpers.push(ccPair.cc_pair_id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="my-auto">
|
||||
<ConnectorTitle
|
||||
connector={ccPair.connector}
|
||||
ccPairId={ccPair.cc_pair_id}
|
||||
ccPairName={ccPair.name}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{isPaidEnterpriseFeaturesEnabled && (
|
||||
<IsPublicGroupSelector
|
||||
formikProps={props}
|
||||
objectName="document set"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex mt-6">
|
||||
<Separator className="my-6" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{user?.role === UserRole.CURATOR ? (
|
||||
<>
|
||||
<ConnectorMultiSelect
|
||||
name="cc_pair_ids"
|
||||
label={`Connectors available to ${
|
||||
userGroups && userGroups.length > 1
|
||||
? "the selected group"
|
||||
: "the group you curate"
|
||||
}`}
|
||||
connectors={visibleCcPairs}
|
||||
selectedIds={props.values.cc_pair_ids}
|
||||
onChange={(selectedIds) => {
|
||||
props.setFieldValue("cc_pair_ids", selectedIds);
|
||||
}}
|
||||
placeholder="Search for connectors..."
|
||||
/>
|
||||
|
||||
<NonSelectableConnectors
|
||||
connectors={nonVisibleCcPairs}
|
||||
title={`Connectors not available to the ${
|
||||
userGroups && userGroups.length > 1
|
||||
? `group${
|
||||
props.values.groups.length > 1 ? "s" : ""
|
||||
} you have selected`
|
||||
: "group you curate"
|
||||
}`}
|
||||
description="Only connectors that are directly assigned to the group you are trying to add the document set to will be available."
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ConnectorMultiSelect
|
||||
name="cc_pair_ids"
|
||||
label="Pick your connectors"
|
||||
connectors={visibleCcPairs}
|
||||
selectedIds={props.values.cc_pair_ids}
|
||||
onChange={(selectedIds) => {
|
||||
props.setFieldValue("cc_pair_ids", selectedIds);
|
||||
}}
|
||||
placeholder="Search for connectors..."
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex mt-6 pt-4 border-t border-neutral-200">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="submit"
|
||||
disabled={props.isSubmitting}
|
||||
className="w-64 mx-auto"
|
||||
className="w-56 mx-auto py-1.5 h-auto text-sm"
|
||||
>
|
||||
{isUpdate ? "Update!" : "Create!"}
|
||||
{isUpdate ? "Update Document Set" : "Create Document Set"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
FiUnlock,
|
||||
FiRefreshCw,
|
||||
FiPauseCircle,
|
||||
FiFilter,
|
||||
FiX,
|
||||
} from "react-icons/fi";
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -41,6 +43,7 @@ import Cookies from "js-cookie";
|
||||
import { TOGGLED_CONNECTORS_COOKIE_NAME } from "@/lib/constants";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
import { ConnectorCredentialPairStatus } from "../../connector/[ccPairId]/types";
|
||||
import { FilterComponent, FilterOptions } from "./FilterComponent";
|
||||
|
||||
function SummaryRow({
|
||||
source,
|
||||
@@ -285,7 +288,26 @@ export function CCPairIndexingStatusTable({
|
||||
return savedState ? JSON.parse(savedState) : {};
|
||||
});
|
||||
|
||||
const { groupedStatuses, sortedSources, groupSummaries } = useMemo(() => {
|
||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||
accessType: null,
|
||||
docsCountFilter: {
|
||||
operator: null,
|
||||
value: null,
|
||||
},
|
||||
lastStatus: null,
|
||||
});
|
||||
|
||||
// Reference to the FilterComponent for resetting its state
|
||||
const filterComponentRef = useRef<{
|
||||
resetFilters: () => void;
|
||||
} | null>(null);
|
||||
|
||||
const {
|
||||
groupedStatuses,
|
||||
sortedSources,
|
||||
groupSummaries,
|
||||
filteredGroupedStatuses,
|
||||
} = useMemo(() => {
|
||||
const grouped: Record<ValidSources, ConnectorIndexingStatus<any, any>[]> =
|
||||
{} as Record<ValidSources, ConnectorIndexingStatus<any, any>[]>;
|
||||
|
||||
@@ -337,12 +359,139 @@ export function CCPairIndexingStatusTable({
|
||||
};
|
||||
});
|
||||
|
||||
// Apply filters to create filtered grouped statuses
|
||||
const filteredGrouped: Record<
|
||||
ValidSources,
|
||||
ConnectorIndexingStatus<any, any>[]
|
||||
> = {} as Record<ValidSources, ConnectorIndexingStatus<any, any>[]>;
|
||||
|
||||
sorted.forEach((source) => {
|
||||
const statuses = grouped[source];
|
||||
|
||||
// Apply filters
|
||||
const filteredStatuses = statuses.filter((status) => {
|
||||
// Filter by access type
|
||||
if (filterOptions.accessType && filterOptions.accessType.length > 0) {
|
||||
if (!filterOptions.accessType.includes(status.access_type)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by last status
|
||||
if (filterOptions.lastStatus && filterOptions.lastStatus.length > 0) {
|
||||
if (
|
||||
!filterOptions.lastStatus.includes(
|
||||
status.last_finished_status as any
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by docs count
|
||||
if (filterOptions.docsCountFilter.operator) {
|
||||
const { operator, value } = filterOptions.docsCountFilter;
|
||||
|
||||
// If only operator is selected (no value), show all
|
||||
if (value === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (operator === ">" && !(status.docs_indexed > value)) {
|
||||
return false;
|
||||
} else if (operator === "<" && !(status.docs_indexed < value)) {
|
||||
return false;
|
||||
} else if (operator === "=" && status.docs_indexed !== value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (filteredStatuses.length > 0) {
|
||||
filteredGrouped[source] = filteredStatuses;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
groupedStatuses: grouped,
|
||||
sortedSources: sorted,
|
||||
groupSummaries: summaries,
|
||||
filteredGroupedStatuses: filteredGrouped,
|
||||
};
|
||||
}, [ccPairsIndexingStatuses, editableCcPairsIndexingStatuses]);
|
||||
}, [ccPairsIndexingStatuses, editableCcPairsIndexingStatuses, filterOptions]);
|
||||
|
||||
// Determine which sources to display based on filters and search
|
||||
const displaySources = useMemo(() => {
|
||||
const hasActiveFilters =
|
||||
(filterOptions.accessType && filterOptions.accessType.length > 0) ||
|
||||
(filterOptions.lastStatus && filterOptions.lastStatus.length > 0) ||
|
||||
filterOptions.docsCountFilter.operator !== null;
|
||||
|
||||
if (hasActiveFilters) {
|
||||
return Object.keys(filteredGroupedStatuses) as ValidSources[];
|
||||
}
|
||||
|
||||
return sortedSources;
|
||||
}, [sortedSources, filteredGroupedStatuses, filterOptions]);
|
||||
|
||||
const handleFilterChange = (newFilters: FilterOptions) => {
|
||||
setFilterOptions(newFilters);
|
||||
|
||||
// Auto-expand sources when filters are applied
|
||||
if (
|
||||
(newFilters.accessType && newFilters.accessType.length > 0) ||
|
||||
(newFilters.lastStatus && newFilters.lastStatus.length > 0) ||
|
||||
newFilters.docsCountFilter.operator !== null
|
||||
) {
|
||||
// We need to wait for the filteredGroupedStatuses to be updated
|
||||
// before we can expand the sources
|
||||
setTimeout(() => {
|
||||
const sourcesToExpand = Object.keys(
|
||||
filteredGroupedStatuses
|
||||
) as ValidSources[];
|
||||
const newConnectorsToggled = { ...connectorsToggled };
|
||||
|
||||
sourcesToExpand.forEach((source) => {
|
||||
newConnectorsToggled[source] = true;
|
||||
});
|
||||
|
||||
setConnectorsToggled(newConnectorsToggled);
|
||||
Cookies.set(
|
||||
TOGGLED_CONNECTORS_COOKIE_NAME,
|
||||
JSON.stringify(newConnectorsToggled)
|
||||
);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllFilters = () => {
|
||||
const emptyFilters: FilterOptions = {
|
||||
accessType: null,
|
||||
docsCountFilter: {
|
||||
operator: null,
|
||||
value: null,
|
||||
},
|
||||
lastStatus: null,
|
||||
};
|
||||
|
||||
setFilterOptions(emptyFilters);
|
||||
|
||||
// Reset the FilterComponent's internal state
|
||||
if (filterComponentRef.current) {
|
||||
filterComponentRef.current.resetFilters();
|
||||
}
|
||||
};
|
||||
|
||||
// Check if filters are active
|
||||
const hasActiveFilters = useMemo(() => {
|
||||
return (
|
||||
(filterOptions.accessType && filterOptions.accessType.length > 0) ||
|
||||
(filterOptions.lastStatus && filterOptions.lastStatus.length > 0) ||
|
||||
filterOptions.docsCountFilter.operator !== null
|
||||
);
|
||||
}, [filterOptions]);
|
||||
|
||||
const toggleSource = (
|
||||
source: ValidSources,
|
||||
@@ -376,127 +525,194 @@ export function CCPairIndexingStatusTable({
|
||||
sortedSources.length;
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<ConnectorRow
|
||||
invisible
|
||||
ccPairsIndexingStatus={{
|
||||
cc_pair_id: 1,
|
||||
name: "Sample File Connector",
|
||||
cc_pair_status: ConnectorCredentialPairStatus.ACTIVE,
|
||||
last_status: "success",
|
||||
connector: {
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<ConnectorRow
|
||||
invisible
|
||||
ccPairsIndexingStatus={{
|
||||
cc_pair_id: 1,
|
||||
name: "Sample File Connector",
|
||||
source: ValidSources.File,
|
||||
input_type: "poll",
|
||||
connector_specific_config: {
|
||||
file_locations: ["/path/to/sample/file.txt"],
|
||||
cc_pair_status: ConnectorCredentialPairStatus.ACTIVE,
|
||||
last_status: "success",
|
||||
connector: {
|
||||
name: "Sample File Connector",
|
||||
source: ValidSources.File,
|
||||
input_type: "poll",
|
||||
connector_specific_config: {
|
||||
file_locations: ["/path/to/sample/file.txt"],
|
||||
},
|
||||
refresh_freq: 86400,
|
||||
prune_freq: null,
|
||||
indexing_start: new Date("2023-07-01T12:00:00Z"),
|
||||
id: 1,
|
||||
credential_ids: [],
|
||||
access_type: "public",
|
||||
time_created: "2023-07-01T12:00:00Z",
|
||||
time_updated: "2023-07-01T12:00:00Z",
|
||||
},
|
||||
credential: {
|
||||
id: 1,
|
||||
name: "Sample Credential",
|
||||
source: ValidSources.File,
|
||||
user_id: "1",
|
||||
time_created: "2023-07-01T12:00:00Z",
|
||||
time_updated: "2023-07-01T12:00:00Z",
|
||||
credential_json: {},
|
||||
admin_public: false,
|
||||
},
|
||||
refresh_freq: 86400,
|
||||
prune_freq: null,
|
||||
indexing_start: new Date("2023-07-01T12:00:00Z"),
|
||||
id: 1,
|
||||
credential_ids: [],
|
||||
access_type: "public",
|
||||
time_created: "2023-07-01T12:00:00Z",
|
||||
time_updated: "2023-07-01T12:00:00Z",
|
||||
},
|
||||
credential: {
|
||||
id: 1,
|
||||
name: "Sample Credential",
|
||||
source: ValidSources.File,
|
||||
user_id: "1",
|
||||
time_created: "2023-07-01T12:00:00Z",
|
||||
time_updated: "2023-07-01T12:00:00Z",
|
||||
credential_json: {},
|
||||
admin_public: false,
|
||||
},
|
||||
access_type: "public",
|
||||
docs_indexed: 1000,
|
||||
last_success: "2023-07-01T12:00:00Z",
|
||||
last_finished_status: "success",
|
||||
latest_index_attempt: null,
|
||||
groups: [], // Add this line
|
||||
}}
|
||||
isEditable={false}
|
||||
/>
|
||||
</TableHeader>
|
||||
<div className="flex -mt-12 items-center w-0 m4 gap-x-2">
|
||||
<input
|
||||
type="text"
|
||||
ref={searchInputRef}
|
||||
placeholder="Search connectors..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="ml-1 w-96 h-9 border border-border flex-none rounded-md bg-background-50 px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
/>
|
||||
docs_indexed: 1000,
|
||||
last_success: "2023-07-01T12:00:00Z",
|
||||
last_finished_status: "success",
|
||||
latest_index_attempt: null,
|
||||
groups: [], // Add this line
|
||||
}}
|
||||
isEditable={false}
|
||||
/>
|
||||
</TableHeader>
|
||||
<div className="flex -mt-12 items-center w-0 m4 gap-x-2">
|
||||
<input
|
||||
type="text"
|
||||
ref={searchInputRef}
|
||||
placeholder="Search connectors..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="ml-1 w-96 h-9 border border-border flex-none rounded-md bg-background-50 px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||
/>
|
||||
|
||||
<Button className="h-9" onClick={() => toggleSources()}>
|
||||
{!shouldExpand ? "Collapse All" : "Expand All"}
|
||||
</Button>
|
||||
</div>
|
||||
<TableBody>
|
||||
{sortedSources
|
||||
.filter(
|
||||
(source) => source != "not_applicable" && source != "ingestion_api"
|
||||
)
|
||||
.map((source, ind) => {
|
||||
const sourceMatches = source
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase());
|
||||
const matchingConnectors = groupedStatuses[source].filter(
|
||||
(status) =>
|
||||
<Button className="h-9" onClick={() => toggleSources()}>
|
||||
{!shouldExpand ? "Collapse All" : "Expand All"}
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<FilterComponent
|
||||
onFilterChange={handleFilterChange}
|
||||
ref={filterComponentRef}
|
||||
/>
|
||||
|
||||
{hasActiveFilters && (
|
||||
<div className="flex flex-none items-center gap-1 ml-2 max-w-[500px]">
|
||||
{filterOptions.accessType &&
|
||||
filterOptions.accessType.length > 0 && (
|
||||
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
|
||||
Access: {filterOptions.accessType.join(", ")}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{filterOptions.lastStatus &&
|
||||
filterOptions.lastStatus.length > 0 && (
|
||||
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
|
||||
Status:{" "}
|
||||
{filterOptions.lastStatus
|
||||
.map((s) => s.replace(/_/g, " "))
|
||||
.join(", ")}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{filterOptions.docsCountFilter.operator &&
|
||||
filterOptions.docsCountFilter.value !== null && (
|
||||
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
|
||||
Docs {filterOptions.docsCountFilter.operator}{" "}
|
||||
{filterOptions.docsCountFilter.value}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{filterOptions.docsCountFilter.operator &&
|
||||
filterOptions.docsCountFilter.value === null && (
|
||||
<Badge variant="secondary" className="px-2 py-0.5 text-xs">
|
||||
Docs {filterOptions.docsCountFilter.operator} any
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="px-2 py-0.5 text-xs border-red-400 bg-red-100 hover:border-red-600 cursor-pointer hover:bg-red-100 dark:hover:bg-red-900"
|
||||
onClick={() => {
|
||||
if (filterComponentRef.current) {
|
||||
filterComponentRef.current.resetFilters();
|
||||
setFilterOptions({
|
||||
accessType: null,
|
||||
docsCountFilter: {
|
||||
operator: null,
|
||||
value: null,
|
||||
},
|
||||
lastStatus: null,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500 dark:text-red-400">Clear</span>
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<TableBody>
|
||||
{displaySources
|
||||
.filter(
|
||||
(source) =>
|
||||
source != "not_applicable" && source != "ingestion_api"
|
||||
)
|
||||
.map((source, ind) => {
|
||||
const sourceMatches = source
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase());
|
||||
|
||||
const statuses =
|
||||
filteredGroupedStatuses[source] || groupedStatuses[source];
|
||||
|
||||
const matchingConnectors = statuses.filter((status) =>
|
||||
(status.name || "")
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase())
|
||||
);
|
||||
if (sourceMatches || matchingConnectors.length > 0) {
|
||||
return (
|
||||
<React.Fragment key={ind}>
|
||||
<br className="mt-4" />
|
||||
<SummaryRow
|
||||
source={source}
|
||||
summary={groupSummaries[source]}
|
||||
isOpen={connectorsToggled[source] || false}
|
||||
onToggle={() => toggleSource(source)}
|
||||
/>
|
||||
{connectorsToggled[source] && (
|
||||
<>
|
||||
<TableRow
|
||||
noHover
|
||||
className="border ! border-border dark:border-neutral-700"
|
||||
>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Last Indexed</TableHead>
|
||||
<TableHead>Activity</TableHead>
|
||||
{isPaidEnterpriseFeaturesEnabled && (
|
||||
<TableHead>Permissions</TableHead>
|
||||
)}
|
||||
<TableHead>Total Docs</TableHead>
|
||||
<TableHead>Last Status</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
{(sourceMatches
|
||||
? groupedStatuses[source]
|
||||
: matchingConnectors
|
||||
).map((ccPairsIndexingStatus) => (
|
||||
<ConnectorRow
|
||||
key={ccPairsIndexingStatus.cc_pair_id}
|
||||
ccPairsIndexingStatus={ccPairsIndexingStatus}
|
||||
isEditable={editableCcPairsIndexingStatuses.some(
|
||||
(e) =>
|
||||
e.cc_pair_id === ccPairsIndexingStatus.cc_pair_id
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
if (sourceMatches || matchingConnectors.length > 0) {
|
||||
return (
|
||||
<React.Fragment key={ind}>
|
||||
<br className="mt-4" />
|
||||
<SummaryRow
|
||||
source={source}
|
||||
summary={groupSummaries[source]}
|
||||
isOpen={connectorsToggled[source] || false}
|
||||
onToggle={() => toggleSource(source)}
|
||||
/>
|
||||
{connectorsToggled[source] && (
|
||||
<>
|
||||
<TableRow className="border border-border dark:border-neutral-700">
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Last Indexed</TableHead>
|
||||
<TableHead>Activity</TableHead>
|
||||
{isPaidEnterpriseFeaturesEnabled && (
|
||||
<TableHead>Permissions</TableHead>
|
||||
)}
|
||||
<TableHead>Total Docs</TableHead>
|
||||
<TableHead>Last Status</TableHead>
|
||||
<TableHead></TableHead>
|
||||
</TableRow>
|
||||
{(sourceMatches ? statuses : matchingConnectors).map(
|
||||
(ccPairsIndexingStatus) => (
|
||||
<ConnectorRow
|
||||
key={ccPairsIndexingStatus.cc_pair_id}
|
||||
ccPairsIndexingStatus={ccPairsIndexingStatus}
|
||||
isEditable={editableCcPairsIndexingStatuses.some(
|
||||
(e) =>
|
||||
e.cc_pair_id ===
|
||||
ccPairsIndexingStatus.cc_pair_id
|
||||
)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
375
web/src/app/admin/indexing/status/FilterComponent.tsx
Normal file
375
web/src/app/admin/indexing/status/FilterComponent.tsx
Normal file
@@ -0,0 +1,375 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useImperativeHandle, forwardRef } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { SortIcon } from "@/components/icons/icons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { AccessType, ValidStatuses } from "@/lib/types";
|
||||
import { FiFilter, FiX, FiCheck } from "react-icons/fi";
|
||||
|
||||
export interface FilterOptions {
|
||||
accessType: AccessType[] | null;
|
||||
docsCountFilter: {
|
||||
operator: ">" | "<" | "=" | null;
|
||||
value: number | null;
|
||||
};
|
||||
lastStatus: ValidStatuses[] | null;
|
||||
}
|
||||
|
||||
interface FilterComponentProps {
|
||||
onFilterChange: (filters: FilterOptions) => void;
|
||||
}
|
||||
|
||||
export const FilterComponent = forwardRef<
|
||||
{ resetFilters: () => void },
|
||||
FilterComponentProps
|
||||
>(({ onFilterChange }, ref) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [filters, setFilters] = useState<FilterOptions>({
|
||||
accessType: null,
|
||||
docsCountFilter: {
|
||||
operator: null,
|
||||
value: null,
|
||||
},
|
||||
lastStatus: null,
|
||||
});
|
||||
|
||||
// Local state for tracking selected filters before applying
|
||||
const [docsOperator, setDocsOperator] = useState<">" | "<" | "=" | null>(
|
||||
null
|
||||
);
|
||||
const [docsValue, setDocsValue] = useState<string>("");
|
||||
const [selectedAccessTypes, setSelectedAccessTypes] = useState<AccessType[]>(
|
||||
[]
|
||||
);
|
||||
const [selectedStatuses, setSelectedStatuses] = useState<ValidStatuses[]>([]);
|
||||
|
||||
// Expose resetFilters method via ref
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetFilters: () => {
|
||||
setDocsOperator(null);
|
||||
setDocsValue("");
|
||||
setSelectedAccessTypes([]);
|
||||
setSelectedStatuses([]);
|
||||
setFilters({
|
||||
accessType: null,
|
||||
docsCountFilter: {
|
||||
operator: null,
|
||||
value: null,
|
||||
},
|
||||
lastStatus: null,
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
const handleAccessTypeChange = (accessType: AccessType) => {
|
||||
const newAccessTypes = selectedAccessTypes.includes(accessType)
|
||||
? selectedAccessTypes.filter((type) => type !== accessType)
|
||||
: [...selectedAccessTypes, accessType];
|
||||
|
||||
setSelectedAccessTypes(newAccessTypes);
|
||||
};
|
||||
|
||||
const handleStatusChange = (status: ValidStatuses) => {
|
||||
const newStatuses = selectedStatuses.includes(status)
|
||||
? selectedStatuses.filter((s) => s !== status)
|
||||
: [...selectedStatuses, status];
|
||||
|
||||
setSelectedStatuses(newStatuses);
|
||||
};
|
||||
|
||||
const handleDocsFilterChange = () => {
|
||||
if (docsOperator && docsValue) {
|
||||
const newFilters = {
|
||||
...filters,
|
||||
accessType: selectedAccessTypes.length > 0 ? selectedAccessTypes : null,
|
||||
lastStatus: selectedStatuses.length > 0 ? selectedStatuses : null,
|
||||
docsCountFilter: {
|
||||
operator: docsOperator,
|
||||
value: parseInt(docsValue),
|
||||
},
|
||||
};
|
||||
|
||||
setFilters(newFilters);
|
||||
onFilterChange(newFilters);
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const applyFilters = () => {
|
||||
const newFilters = {
|
||||
...filters,
|
||||
accessType: selectedAccessTypes.length > 0 ? selectedAccessTypes : null,
|
||||
lastStatus: selectedStatuses.length > 0 ? selectedStatuses : null,
|
||||
docsCountFilter: {
|
||||
operator: docsOperator,
|
||||
value: docsValue ? parseInt(docsValue) : null,
|
||||
},
|
||||
};
|
||||
|
||||
setFilters(newFilters);
|
||||
onFilterChange(newFilters);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const clearFilters = () => {
|
||||
setSelectedAccessTypes([]);
|
||||
setSelectedStatuses([]);
|
||||
setDocsOperator(null);
|
||||
setDocsValue("");
|
||||
|
||||
const newFilters = {
|
||||
accessType: null,
|
||||
docsCountFilter: {
|
||||
operator: null,
|
||||
value: null,
|
||||
},
|
||||
lastStatus: null,
|
||||
};
|
||||
|
||||
setFilters(newFilters);
|
||||
onFilterChange(newFilters);
|
||||
};
|
||||
|
||||
// Sync local state with filters when dropdown opens
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (open) {
|
||||
// When opening, initialize local state from current filters
|
||||
setSelectedAccessTypes(filters.accessType || []);
|
||||
setSelectedStatuses(filters.lastStatus || []);
|
||||
setDocsOperator(filters.docsCountFilter.operator);
|
||||
setDocsValue(
|
||||
filters.docsCountFilter.value !== null
|
||||
? filters.docsCountFilter.value.toString()
|
||||
: ""
|
||||
);
|
||||
}
|
||||
setIsOpen(open);
|
||||
};
|
||||
|
||||
const hasActiveFilters =
|
||||
(filters.accessType && filters.accessType.length > 0) ||
|
||||
(filters.lastStatus && filters.lastStatus.length > 0) ||
|
||||
filters.docsCountFilter.operator !== null;
|
||||
|
||||
// Get active filter count for badge
|
||||
const getActiveFilterCount = () => {
|
||||
let count = 0;
|
||||
if (filters.accessType && filters.accessType.length > 0) count++;
|
||||
if (filters.lastStatus && filters.lastStatus.length > 0) count++;
|
||||
if (filters.docsCountFilter.operator !== null) count++;
|
||||
return count;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<DropdownMenu open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={`p-2 h-9 ${
|
||||
hasActiveFilters ? "border-primary bg-primary/5" : ""
|
||||
}`}
|
||||
>
|
||||
<SortIcon size={20} className="text-neutral-800" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-72"
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<div className="flex items-center justify-between px-2 py-1.5">
|
||||
<DropdownMenuLabel className="text-base font-medium">
|
||||
Filter Connectors
|
||||
</DropdownMenuLabel>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="px-2 py-1.5 text-xs text-muted-foreground">
|
||||
Access Type
|
||||
</DropdownMenuLabel>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedAccessTypes.includes("public")}
|
||||
onCheckedChange={() => handleAccessTypeChange("public")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Public
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedAccessTypes.includes("private")}
|
||||
onCheckedChange={() => handleAccessTypeChange("private")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Private
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedAccessTypes.includes("sync")}
|
||||
onCheckedChange={() => handleAccessTypeChange("sync")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Auto-Sync
|
||||
</DropdownMenuCheckboxItem>
|
||||
</div>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="px-2 py-1.5 text-xs text-muted-foreground">
|
||||
Last Status
|
||||
</DropdownMenuLabel>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedStatuses.includes("success")}
|
||||
onCheckedChange={() => handleStatusChange("success")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Success
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedStatuses.includes("failed")}
|
||||
onCheckedChange={() => handleStatusChange("failed")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Failed
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedStatuses.includes("in_progress")}
|
||||
onCheckedChange={() => handleStatusChange("in_progress")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
In Progress
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedStatuses.includes("not_started")}
|
||||
onCheckedChange={() => handleStatusChange("not_started")}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Not Started
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={selectedStatuses.includes("completed_with_errors")}
|
||||
onCheckedChange={() =>
|
||||
handleStatusChange("completed_with_errors")
|
||||
}
|
||||
className="flex items-center justify-between"
|
||||
onSelect={(e) => e.preventDefault()}
|
||||
>
|
||||
Completed with Errors
|
||||
</DropdownMenuCheckboxItem>
|
||||
</div>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="px-2 py-1.5 text-xs text-muted-foreground">
|
||||
Document Count
|
||||
</DropdownMenuLabel>
|
||||
<div
|
||||
className="flex items-center px-2 py-2 gap-2"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={docsOperator === ">" ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDocsOperator(docsOperator === ">" ? null : ">");
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
>
|
||||
</Button>
|
||||
<Button
|
||||
variant={docsOperator === "<" ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDocsOperator(docsOperator === "<" ? null : "<");
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<
|
||||
</Button>
|
||||
<Button
|
||||
variant={docsOperator === "=" ? "default" : "outline"}
|
||||
size="sm"
|
||||
className="h-8 px-2"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDocsOperator(docsOperator === "=" ? null : "=");
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
=
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Count"
|
||||
value={docsValue}
|
||||
onChange={(e) => setDocsValue(e.target.value)}
|
||||
className="h-8 w-full"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-2 py-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full h-8"
|
||||
disabled={false}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
applyFilters();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{hasActiveFilters && (
|
||||
<div className="absolute -top-1 -right-1">
|
||||
<Badge className="h-2 bg-red-400 border-red-400 w-2 p-0 border-2 flex items-center justify-center" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
FilterComponent.displayName = "FilterComponent";
|
||||
@@ -403,7 +403,8 @@ export function ChatInputBar({
|
||||
setTabbingIconIndex((tabbingIconIndex) =>
|
||||
Math.min(
|
||||
tabbingIconIndex + 1,
|
||||
showPrompts ? filteredPrompts.length : assistantTagOptions.length
|
||||
// showPrompts ? filteredPrompts.length :
|
||||
assistantTagOptions.length
|
||||
)
|
||||
);
|
||||
} else if (e.key === "ArrowUp") {
|
||||
@@ -436,8 +437,8 @@ export function ChatInputBar({
|
||||
<button
|
||||
key={index}
|
||||
className={`px-2 ${
|
||||
tabbingIconIndex == index && "bg-background-dark/75"
|
||||
} rounded items-center rounded-lg content-start flex gap-x-1 py-2 w-full hover:bg-background-dark/90 cursor-pointer`}
|
||||
tabbingIconIndex == index && "bg-neutral-200"
|
||||
} rounded items-center rounded-lg content-start flex gap-x-1 py-2 w-full hover:bg-neutral-200/90 cursor-pointer`}
|
||||
onClick={() => {
|
||||
updatedTaggedAssistant(currentAssistant);
|
||||
}}
|
||||
@@ -459,8 +460,8 @@ export function ChatInputBar({
|
||||
target="_self"
|
||||
className={`${
|
||||
tabbingIconIndex == assistantTagOptions.length &&
|
||||
"bg-background-dark/75"
|
||||
} rounded rounded-lg px-3 flex gap-x-1 py-2 w-full items-center hover:bg-background-dark/90 cursor-pointer`}
|
||||
"bg-neutral-200"
|
||||
} rounded rounded-lg px-3 flex gap-x-1 py-2 w-full items-center hover:bg-neutral-200/90 cursor-pointer`}
|
||||
href="/assistants/new"
|
||||
>
|
||||
<FiPlus size={17} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ConnectorIndexingStatus, ConnectorStatus } from "@/lib/types";
|
||||
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
|
||||
import { ConnectorStatus } from "@/lib/types";
|
||||
import { ConnectorMultiSelect } from "@/components/ConnectorMultiSelect";
|
||||
|
||||
interface ConnectorEditorProps {
|
||||
selectedCCPairIds: number[];
|
||||
@@ -12,55 +12,20 @@ export const ConnectorEditor = ({
|
||||
setSetCCPairIds,
|
||||
allCCPairs,
|
||||
}: ConnectorEditorProps) => {
|
||||
// Filter out public docs, since they don't make sense as part of a group
|
||||
const privateCCPairs = allCCPairs.filter(
|
||||
(ccPair) => ccPair.access_type === "private"
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-3 flex gap-2 flex-wrap">
|
||||
{allCCPairs
|
||||
// remove public docs, since they don't make sense as part of a group
|
||||
.filter((ccPair) => !(ccPair.access_type === "public"))
|
||||
.map((ccPair) => {
|
||||
const ind = selectedCCPairIds.indexOf(ccPair.cc_pair_id);
|
||||
const isSelected = ind !== -1;
|
||||
return (
|
||||
<div
|
||||
key={`${ccPair.connector.id}-${ccPair.credential.id}`}
|
||||
className={
|
||||
`
|
||||
px-3
|
||||
py-1
|
||||
rounded-lg
|
||||
border
|
||||
border-border
|
||||
w-fit
|
||||
flex
|
||||
cursor-pointer ` +
|
||||
(isSelected
|
||||
? " bg-accent-background-hovered"
|
||||
: " hover:bg-accent-background")
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
setSetCCPairIds(
|
||||
selectedCCPairIds.filter(
|
||||
(ccPairId) => ccPairId !== ccPair.cc_pair_id
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setSetCCPairIds([...selectedCCPairIds, ccPair.cc_pair_id]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="my-auto">
|
||||
<ConnectorTitle
|
||||
connector={ccPair.connector}
|
||||
ccPairId={ccPair.cc_pair_id}
|
||||
ccPairName={ccPair.name}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ConnectorMultiSelect
|
||||
name="connectors"
|
||||
label="Connectors"
|
||||
connectors={privateCCPairs}
|
||||
selectedIds={selectedCCPairIds}
|
||||
onChange={setSetCCPairIds}
|
||||
placeholder="Search for connectors..."
|
||||
showError={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Button } from "@/components/Button";
|
||||
import { SearchMultiSelectDropdown } from "@/components/Dropdown";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { useState } from "react";
|
||||
import { FiPlus, FiX } from "react-icons/fi";
|
||||
import { FiX } from "react-icons/fi";
|
||||
import { updateUserGroup } from "./lib";
|
||||
import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { ConnectorStatus, UserGroup } from "@/lib/types";
|
||||
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
|
||||
import { Connector } from "@/lib/connectors/connectors";
|
||||
import { ConnectorMultiSelect } from "@/components/ConnectorMultiSelect";
|
||||
import { Form } from "formik";
|
||||
|
||||
interface AddConnectorFormProps {
|
||||
ccPairs: ConnectorStatus<any, any>[];
|
||||
userGroup: UserGroup;
|
||||
@@ -23,132 +25,68 @@ export const AddConnectorForm: React.FC<AddConnectorFormProps> = ({
|
||||
}) => {
|
||||
const [selectedCCPairIds, setSelectedCCPairIds] = useState<number[]>([]);
|
||||
|
||||
const selectedCCPairs = ccPairs.filter((ccPair) =>
|
||||
selectedCCPairIds.includes(ccPair.cc_pair_id)
|
||||
);
|
||||
return (
|
||||
<Modal title="Add New Connector" onOutsideClick={() => onClose()}>
|
||||
<div className="px-6 pt-4 pb-12">
|
||||
<div className="mb-2 flex flex-wrap gap-x-2">
|
||||
{selectedCCPairs.length > 0 &&
|
||||
selectedCCPairs.map((ccPair) => (
|
||||
<div
|
||||
key={ccPair.cc_pair_id}
|
||||
onClick={() => {
|
||||
setSelectedCCPairIds(
|
||||
selectedCCPairIds.filter(
|
||||
(ccPairId) => ccPairId !== ccPair.cc_pair_id
|
||||
)
|
||||
);
|
||||
}}
|
||||
className={`
|
||||
flex
|
||||
rounded-lg
|
||||
px-2
|
||||
py-1
|
||||
my-1
|
||||
border
|
||||
border-border
|
||||
hover:bg-accent-background-hovered
|
||||
cursor-pointer`}
|
||||
>
|
||||
<ConnectorTitle
|
||||
ccPairId={ccPair.cc_pair_id}
|
||||
ccPairName={ccPair.name}
|
||||
connector={ccPair.connector}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
<FiX className="ml-1 my-auto" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
// Filter out ccPairs that are already in the user group and are not private
|
||||
const availableCCPairs = ccPairs
|
||||
.filter(
|
||||
(ccPair) =>
|
||||
!userGroup.cc_pairs
|
||||
.map((userGroupCCPair) => userGroupCCPair.id)
|
||||
.includes(ccPair.cc_pair_id)
|
||||
)
|
||||
.filter((ccPair) => ccPair.access_type === "private");
|
||||
|
||||
<div className="flex">
|
||||
<SearchMultiSelectDropdown
|
||||
options={ccPairs
|
||||
.filter(
|
||||
(ccPair) =>
|
||||
!selectedCCPairIds.includes(ccPair.cc_pair_id) &&
|
||||
!userGroup.cc_pairs
|
||||
.map((userGroupCCPair) => userGroupCCPair.id)
|
||||
.includes(ccPair.cc_pair_id)
|
||||
)
|
||||
// remove public and synced docs, since they don't make sense as part of a group
|
||||
.filter((ccPair) => ccPair.access_type === "private")
|
||||
.map((ccPair) => {
|
||||
return {
|
||||
name: ccPair.name?.toString() || "",
|
||||
value: ccPair.cc_pair_id?.toString(),
|
||||
metadata: {
|
||||
ccPairId: ccPair.cc_pair_id,
|
||||
connector: ccPair.connector,
|
||||
},
|
||||
};
|
||||
})}
|
||||
onSelect={(option) => {
|
||||
setSelectedCCPairIds([
|
||||
...Array.from(
|
||||
new Set([
|
||||
...selectedCCPairIds,
|
||||
parseInt(option.value as string),
|
||||
])
|
||||
),
|
||||
]);
|
||||
}}
|
||||
itemComponent={({ option }) => (
|
||||
<div className="flex px-4 py-2.5 hover:bg-accent-background-hovered cursor-pointer">
|
||||
<div className="my-auto">
|
||||
<ConnectorTitle
|
||||
ccPairId={option?.metadata?.ccPairId as number}
|
||||
ccPairName={option.name}
|
||||
connector={option?.metadata?.connector as Connector<any>}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-auto my-auto">
|
||||
<FiPlus />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
className="ml-3 flex-nowrap w-48"
|
||||
onClick={async () => {
|
||||
const newCCPairIds = [
|
||||
...Array.from(
|
||||
new Set(
|
||||
userGroup.cc_pairs
|
||||
.map((ccPair) => ccPair.id)
|
||||
.concat(selectedCCPairIds)
|
||||
)
|
||||
),
|
||||
];
|
||||
const response = await updateUserGroup(userGroup.id, {
|
||||
user_ids: userGroup.users.map((user) => user.id),
|
||||
cc_pair_ids: newCCPairIds,
|
||||
return (
|
||||
<Modal
|
||||
className="max-w-3xl"
|
||||
title="Add New Connector"
|
||||
onOutsideClick={() => onClose()}
|
||||
>
|
||||
<div className="px-6 pt-4">
|
||||
<ConnectorMultiSelect
|
||||
name="connectors"
|
||||
label="Select Connectors"
|
||||
connectors={availableCCPairs}
|
||||
selectedIds={selectedCCPairIds}
|
||||
onChange={setSelectedCCPairIds}
|
||||
placeholder="Search for connectors to add..."
|
||||
showError={false}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="mt-4 flex-nowrap w-48"
|
||||
onClick={async () => {
|
||||
const newCCPairIds = [
|
||||
...Array.from(
|
||||
new Set(
|
||||
userGroup.cc_pairs
|
||||
.map((ccPair) => ccPair.id)
|
||||
.concat(selectedCCPairIds)
|
||||
)
|
||||
),
|
||||
];
|
||||
const response = await updateUserGroup(userGroup.id, {
|
||||
user_ids: userGroup.users.map((user) => user.id),
|
||||
cc_pair_ids: newCCPairIds,
|
||||
});
|
||||
if (response.ok) {
|
||||
setPopup({
|
||||
message: "Successfully added connectors to group",
|
||||
type: "success",
|
||||
});
|
||||
if (response.ok) {
|
||||
setPopup({
|
||||
message: "Successfully added users to group",
|
||||
type: "success",
|
||||
});
|
||||
onClose();
|
||||
} else {
|
||||
const responseJson = await response.json();
|
||||
const errorMsg = responseJson.detail || responseJson.message;
|
||||
setPopup({
|
||||
message: `Failed to add users to group - ${errorMsg}`,
|
||||
type: "error",
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add Connectors
|
||||
</Button>
|
||||
</div>
|
||||
onClose();
|
||||
} else {
|
||||
const responseJson = await response.json();
|
||||
const errorMsg = responseJson.detail || responseJson.message;
|
||||
setPopup({
|
||||
message: `Failed to add connectors to group - ${errorMsg}`,
|
||||
type: "error",
|
||||
});
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Add Connectors
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
232
web/src/components/ConnectorMultiSelect.tsx
Normal file
232
web/src/components/ConnectorMultiSelect.tsx
Normal file
@@ -0,0 +1,232 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { ConnectorStatus } from "@/lib/types";
|
||||
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
|
||||
import { X, Search } from "lucide-react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ErrorMessage } from "formik";
|
||||
|
||||
interface ConnectorMultiSelectProps {
|
||||
name: string;
|
||||
label: string;
|
||||
connectors: ConnectorStatus<any, any>[];
|
||||
selectedIds: number[];
|
||||
onChange: (selectedIds: number[]) => void;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
showError?: boolean;
|
||||
}
|
||||
|
||||
export const ConnectorMultiSelect = ({
|
||||
name,
|
||||
label,
|
||||
connectors,
|
||||
selectedIds,
|
||||
onChange,
|
||||
disabled = false,
|
||||
placeholder = "Search connectors...",
|
||||
showError = false,
|
||||
}: ConnectorMultiSelectProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const selectedConnectors = connectors.filter((connector) =>
|
||||
selectedIds.includes(connector.cc_pair_id)
|
||||
);
|
||||
|
||||
const unselectedConnectors = connectors.filter(
|
||||
(connector) => !selectedIds.includes(connector.cc_pair_id)
|
||||
);
|
||||
|
||||
const allConnectorsSelected = unselectedConnectors.length === 0;
|
||||
|
||||
const filteredUnselectedConnectors = unselectedConnectors.filter(
|
||||
(connector) => {
|
||||
const connectorName = connector.name || connector.connector.source;
|
||||
return connectorName.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (allConnectorsSelected && open) {
|
||||
setOpen(false);
|
||||
inputRef.current?.blur();
|
||||
setSearchQuery("");
|
||||
}
|
||||
}, [allConnectorsSelected, open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (allConnectorsSelected) {
|
||||
inputRef.current?.blur();
|
||||
setSearchQuery("");
|
||||
}
|
||||
}, [allConnectorsSelected, selectedIds]);
|
||||
|
||||
const selectConnector = (connectorId: number) => {
|
||||
const newSelectedIds = [...selectedIds, connectorId];
|
||||
onChange(newSelectedIds);
|
||||
setSearchQuery("");
|
||||
|
||||
const willAllBeSelected = connectors.length === newSelectedIds.length;
|
||||
|
||||
if (!willAllBeSelected) {
|
||||
setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const removeConnector = (connectorId: number) => {
|
||||
onChange(selectedIds.filter((id) => id !== connectorId));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
dropdownRef.current &&
|
||||
!dropdownRef.current.contains(event.target as Node) &&
|
||||
inputRef.current !== event.target &&
|
||||
!inputRef.current?.contains(event.target as Node)
|
||||
) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const effectivePlaceholder = allConnectorsSelected
|
||||
? "All connectors selected"
|
||||
: placeholder;
|
||||
|
||||
const isInputDisabled = disabled || allConnectorsSelected;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full space-y-2 mb-4">
|
||||
{label && <Label className="text-base font-medium">{label}</Label>}
|
||||
|
||||
<p className="text-xs text-neutral-500 ">
|
||||
All documents indexed by the selected connectors will be part of this
|
||||
document set.
|
||||
</p>
|
||||
<div className="relative">
|
||||
<div
|
||||
className={`flex items-center border border-input rounded-md border border-neutral-200 ${
|
||||
allConnectorsSelected ? "bg-neutral-50" : ""
|
||||
} focus-within:ring-1 focus-within:ring-ring focus-within:border-neutral-400 transition-colors`}
|
||||
>
|
||||
<Search className="absolute left-3 h-4 w-4 text-neutral-500" />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
setOpen(true);
|
||||
}}
|
||||
onFocus={() => {
|
||||
if (!allConnectorsSelected) {
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={effectivePlaceholder}
|
||||
className={`h-9 w-full pl-9 pr-10 py-2 bg-transparent text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50 ${
|
||||
allConnectorsSelected ? "text-neutral-500" : ""
|
||||
}`}
|
||||
disabled={isInputDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{open && !allConnectorsSelected && (
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="absolute z-50 w-full mt-1 rounded-md border border-neutral-200 bg-white shadow-md default-scrollbar max-h-[300px] overflow-auto"
|
||||
>
|
||||
{filteredUnselectedConnectors.length === 0 ? (
|
||||
<div className="py-4 text-center text-xs text-neutral-500">
|
||||
{searchQuery
|
||||
? "No matching connectors found"
|
||||
: "No more connectors available"}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{filteredUnselectedConnectors.map((connector) => (
|
||||
<div
|
||||
key={connector.cc_pair_id}
|
||||
className="flex items-center justify-between py-2 px-3 cursor-pointer hover:bg-neutral-50 text-xs"
|
||||
onClick={() => selectConnector(connector.cc_pair_id)}
|
||||
>
|
||||
<div className="flex items-center truncate mr-2">
|
||||
<ConnectorTitle
|
||||
connector={connector.connector}
|
||||
ccPairId={connector.cc_pair_id}
|
||||
ccPairName={connector.name}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedConnectors.length > 0 ? (
|
||||
<div className="mt-3 ">
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{selectedConnectors.map((connector) => (
|
||||
<div
|
||||
key={connector.cc_pair_id}
|
||||
className="flex items-center bg-white rounded-md border border-neutral-300 transition-all px-2 py-1 max-w-full group text-xs"
|
||||
>
|
||||
<div className="flex items-center overflow-hidden">
|
||||
<div className="flex-shrink-0 text-xs">
|
||||
<ConnectorTitle
|
||||
connector={connector.connector}
|
||||
ccPairId={connector.cc_pair_id}
|
||||
ccPairName={connector.name}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
className="ml-1 flex-shrink-0 rounded-full w-4 h-4 flex items-center justify-center bg-neutral-100 text-neutral-500 hover:bg-neutral-200 hover:text-neutral-700 transition-colors group-hover:bg-neutral-200"
|
||||
onClick={() => removeConnector(connector.cc_pair_id)}
|
||||
aria-label="Remove connector"
|
||||
>
|
||||
<X className="h-2.5 w-2.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 p-3 border border-dashed border-neutral-300 rounded-md bg-neutral-50 text-neutral-500 text-xs">
|
||||
No connectors selected. Search and select connectors above.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showError && (
|
||||
<ErrorMessage
|
||||
name={name}
|
||||
component="div"
|
||||
className="text-red-500 text-xs mt-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
52
web/src/components/NonSelectableConnectors.tsx
Normal file
52
web/src/components/NonSelectableConnectors.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
import { ConnectorStatus } from "@/lib/types";
|
||||
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { LockIcon } from "lucide-react";
|
||||
|
||||
interface NonSelectableConnectorsProps {
|
||||
connectors: ConnectorStatus<any, any>[];
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const NonSelectableConnectors = ({
|
||||
connectors,
|
||||
title,
|
||||
description,
|
||||
}: NonSelectableConnectorsProps) => {
|
||||
if (connectors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 mb-4">
|
||||
<Label className="text-base font-medium mb-1">{title}</Label>
|
||||
<p className="text-xs text-neutral-500 mb-3">{description}</p>
|
||||
<div className="p-3 border border-dashed border-neutral-300 rounded-md bg-neutral-50">
|
||||
<div className="text-xs font-medium text-neutral-700 mb-2 flex items-center">
|
||||
<LockIcon className="h-3.5 w-3.5 mr-1.5 text-neutral-500" />
|
||||
Unavailable connectors:
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{connectors.map((connector) => (
|
||||
<div
|
||||
key={`${connector.connector.id}-${connector.credential.id}`}
|
||||
className="flex items-center px-2 py-1 cursor-not-allowed opacity-80 bg-white border border-neutral-300 rounded-md text-xs"
|
||||
>
|
||||
<div className="flex items-center max-w-[200px] text-xs">
|
||||
<ConnectorTitle
|
||||
connector={connector.connector}
|
||||
ccPairId={connector.cc_pair_id}
|
||||
ccPairName={connector.name}
|
||||
isLink={false}
|
||||
showMetadata={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -20,6 +20,7 @@ interface ConnectorTitleProps {
|
||||
owner?: string;
|
||||
isLink?: boolean;
|
||||
showMetadata?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ConnectorTitle = ({
|
||||
@@ -30,6 +31,7 @@ export const ConnectorTitle = ({
|
||||
isPublic = true,
|
||||
isLink = true,
|
||||
showMetadata = true,
|
||||
className = "",
|
||||
}: ConnectorTitleProps) => {
|
||||
const sourceMetadata = getSourceMetadata(connector.source);
|
||||
|
||||
@@ -88,17 +90,17 @@ export const ConnectorTitle = ({
|
||||
);
|
||||
}
|
||||
|
||||
const mainSectionClassName = "text-blue-500 dark:text-blue-100 flex w-fit";
|
||||
const mainSectionClassName = `text-blue-500 dark:text-blue-100 flex w-fit ${className}`;
|
||||
const mainDisplay = (
|
||||
<>
|
||||
{sourceMetadata.icon({ size: 20 })}
|
||||
<div className="ml-1 my-auto">
|
||||
{sourceMetadata.icon({ size: 16 })}
|
||||
<div className="ml-1 my-auto text-xs font-medium truncate">
|
||||
{ccPairName || sourceMetadata.displayName}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<div className="my-auto">
|
||||
<div className="my-auto max-w-full">
|
||||
{isLink ? (
|
||||
<Link
|
||||
className={mainSectionClassName}
|
||||
@@ -110,10 +112,10 @@ export const ConnectorTitle = ({
|
||||
<div className={mainSectionClassName}>{mainDisplay}</div>
|
||||
)}
|
||||
{showMetadata && additionalMetadata.size > 0 && (
|
||||
<div className="text-xs mt-1">
|
||||
<div className="text-[10px] mt-0.5 text-gray-600 dark:text-gray-400">
|
||||
{Array.from(additionalMetadata.entries()).map(([key, value]) => {
|
||||
return (
|
||||
<div key={key}>
|
||||
<div key={key} className="truncate">
|
||||
<i>{key}:</i> {value}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3278,18 +3278,25 @@ export const CirclingArrowIcon = ({
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
// <svg
|
||||
// style={{ width: `${size}px`, height: `${size}px` }}
|
||||
// className={`w-[${size}px] h-[${size}px] ` + className}
|
||||
// viewBox="0 0 112.62 120.72"
|
||||
// data-name="Layer 1"
|
||||
// xmlns="http://www.w3.org/2000/svg"
|
||||
// >
|
||||
// <path
|
||||
// strokeWidth={100}
|
||||
// d="M11.64,100.12l-.4-.47-1.06,8.63a5.08,5.08,0,0,1-1.92,3.41A5.11,5.11,0,0,1,0,107L2.79,84.65v-.07a3.28,3.28,0,0,1,.08-.41h0A5.09,5.09,0,0,1,9,80.39q11.22,2.53,22.42,5.15a5,5,0,0,1,3.17,2.25,5.14,5.14,0,0,1,.64,3.84v0a5,5,0,0,1-2.25,3.16,5.08,5.08,0,0,1-3.83.65c-3.31-.75-6.62-1.52-9.92-2.28a40.71,40.71,0,0,0,2.84,3,50.09,50.09,0,0,0,26.23,13.49,48.67,48.67,0,0,0,14.71.34A47.35,47.35,0,0,0,77,106h0q2.52-1.19,4.83-2.54c1.56-.93,3.07-1.92,4.51-3a50.8,50.8,0,0,0,8.56-7.88,48.92,48.92,0,0,0,6.39-9.45l.56-1.1,10,2.69-.8,1.66a58.64,58.64,0,0,1-7.9,12.24,61.28,61.28,0,0,1-10.81,10.1c-1.68,1.23-3.46,2.4-5.32,3.5s-3.73,2.07-5.74,3a58,58,0,0,1-17,5,58.56,58.56,0,0,1-17.79-.39,60.21,60.21,0,0,1-31.58-16.26c-1.2-1.16-2.26-2.31-3.24-3.45ZM101,20.6l.4.47,1-8.63a5.11,5.11,0,1,1,10.14,1.26l-2.74,22.37,0,.07c0,.13,0,.27-.07.41h0a5.09,5.09,0,0,1-6.08,3.78c-7.47-1.69-15-3.4-22.42-5.15a5,5,0,0,1-3.16-2.25,5.1,5.1,0,0,1-.65-3.84v0a5,5,0,0,1,2.25-3.16,5.1,5.1,0,0,1,3.84-.65c3.31.75,6.61,1.52,9.92,2.28-.84-1-1.77-2-2.84-3.05a50.09,50.09,0,0,0-12.13-8.73A49.49,49.49,0,0,0,64.37,11a48.6,48.6,0,0,0-14.7-.34,47.26,47.26,0,0,0-14,4.1h0q-2.53,1.18-4.83,2.54c-1.57.93-3.07,1.92-4.52,3a50.34,50.34,0,0,0-8.55,7.88,48,48,0,0,0-6.39,9.45l-.57,1.1L.76,36l.8-1.66A58.9,58.9,0,0,1,9.46,22.1,61.63,61.63,0,0,1,20.27,12q2.54-1.85,5.32-3.5c1.81-1.06,3.73-2.07,5.74-3a58,58,0,0,1,17-5A58.56,58.56,0,0,1,66.16.89a59.77,59.77,0,0,1,17,5.74A60.4,60.4,0,0,1,97.75,17.15c1.19,1.16,2.26,2.31,3.24,3.45Z"
|
||||
// />
|
||||
// </svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const SortIcon = ({
|
||||
size = 24,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
style={{ width: `${size}px`, height: `${size}px` }}
|
||||
className={`w-[${size}px] h-[${size}px] ` + className}
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M22 18.605a.75.75 0 0 1-.75.75h-5.1a2.93 2.93 0 0 1-5.66 0H2.75a.75.75 0 1 1 0-1.5h7.74a2.93 2.93 0 0 1 5.66 0h5.1a.75.75 0 0 1 .75.75m0-13.21a.75.75 0 0 1-.75.75H18.8a2.93 2.93 0 0 1-5.66 0H2.75a.75.75 0 1 1 0-1.5h10.39a2.93 2.93 0 0 1 5.66 0h2.45a.74.74 0 0 1 .75.75m0 6.6a.74.74 0 0 1-.75.75H9.55a2.93 2.93 0 0 1-5.66 0H2.75a.75.75 0 1 1 0-1.5h1.14a2.93 2.93 0 0 1 5.66 0h11.7a.75.75 0 0 1 .75.75"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
153
web/src/components/ui/command.tsx
Normal file
153
web/src/components/ui/command.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { type DialogProps } from "@radix-ui/react-dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-neutral-950 dark:bg-neutral-950 dark:text-neutral-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-neutral-500 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 dark:[&_[cmdk-group-heading]]:text-neutral-400">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-neutral-500 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-neutral-400",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-neutral-950 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-neutral-500 dark:text-neutral-50 dark:[&_[cmdk-group-heading]]:text-neutral-400",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-neutral-200 dark:bg-neutral-800", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-neutral-100 data-[selected=true]:text-neutral-900 data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:data-[selected='true']:bg-neutral-800 dark:data-[selected=true]:text-neutral-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-neutral-500 dark:text-neutral-400",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CommandShortcut.displayName = "CommandShortcut";
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
Reference in New Issue
Block a user