mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-04 10:05:53 +02:00
Merge branch 'main' of https://github.com/fosrl/pangolin
This commit is contained in:
commit
6530fff87e
33 changed files with 827 additions and 608 deletions
|
@ -4,7 +4,7 @@ WORKDIR /app
|
|||
|
||||
COPY package.json ./
|
||||
|
||||
RUN npm install --legacy-peer-deps
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
|
@ -20,7 +20,7 @@ WORKDIR /app
|
|||
|
||||
COPY package.json ./
|
||||
|
||||
RUN npm install --omit=dev --legacy-peer-deps
|
||||
RUN npm install --omit=dev
|
||||
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
|
118
package.json
118
package.json
|
@ -13,95 +13,97 @@
|
|||
"email": "email dev --dir server/emails/templates --port 3005"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "3.9.0",
|
||||
"@node-rs/argon2": "1.8.3",
|
||||
"@hookform/resolvers": "3.9.1",
|
||||
"@node-rs/argon2": "2.0.2",
|
||||
"@oslojs/crypto": "1.0.1",
|
||||
"@oslojs/encoding": "1.1.0",
|
||||
"@radix-ui/react-avatar": "1.1.1",
|
||||
"@radix-ui/react-checkbox": "1.1.2",
|
||||
"@radix-ui/react-dialog": "1.1.2",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.2",
|
||||
"@radix-ui/react-icons": "1.3.0",
|
||||
"@radix-ui/react-label": "2.1.0",
|
||||
"@radix-ui/react-popover": "1.1.2",
|
||||
"@radix-ui/react-radio-group": "1.2.1",
|
||||
"@radix-ui/react-select": "2.1.2",
|
||||
"@radix-ui/react-separator": "1.1.0",
|
||||
"@radix-ui/react-slot": "1.1.0",
|
||||
"@radix-ui/react-switch": "1.1.1",
|
||||
"@radix-ui/react-tabs": "1.1.1",
|
||||
"@radix-ui/react-toast": "1.2.2",
|
||||
"@react-email/components": "0.0.28",
|
||||
"@react-email/tailwind": "1.0.2",
|
||||
"@tanstack/react-table": "8.20.5",
|
||||
"axios": "1.7.7",
|
||||
"better-sqlite3": "11.3.0",
|
||||
"class-variance-authority": "0.7.0",
|
||||
"@radix-ui/react-avatar": "1.1.2",
|
||||
"@radix-ui/react-checkbox": "1.1.3",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.4",
|
||||
"@radix-ui/react-icons": "1.3.2",
|
||||
"@radix-ui/react-label": "2.1.1",
|
||||
"@radix-ui/react-popover": "1.1.4",
|
||||
"@radix-ui/react-radio-group": "1.2.2",
|
||||
"@radix-ui/react-select": "2.1.4",
|
||||
"@radix-ui/react-separator": "1.1.1",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-switch": "1.1.2",
|
||||
"@radix-ui/react-tabs": "1.1.2",
|
||||
"@radix-ui/react-toast": "1.2.4",
|
||||
"@react-email/components": "0.0.31",
|
||||
"@react-email/tailwind": "1.0.4",
|
||||
"@tanstack/react-table": "8.20.6",
|
||||
"axios": "1.7.9",
|
||||
"better-sqlite3": "11.7.0",
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"cookie-parser": "1.4.6",
|
||||
"cmdk": "1.0.4",
|
||||
"cookie-parser": "1.4.7",
|
||||
"cors": "2.8.5",
|
||||
"drizzle-orm": "0.33.0",
|
||||
"emblor": "1.4.6",
|
||||
"eslint": "9.15.0",
|
||||
"eslint-config-next": "15.0.3",
|
||||
"express": "4.21.0",
|
||||
"express-rate-limit": "7.4.0",
|
||||
"drizzle-orm": "0.38.3",
|
||||
"emblor": "1.4.7",
|
||||
"eslint": "9.17.0",
|
||||
"eslint-config-next": "15.1.3",
|
||||
"express": "4.21.2",
|
||||
"express-rate-limit": "7.5.0",
|
||||
"glob": "11.0.0",
|
||||
"helmet": "7.1.0",
|
||||
"helmet": "8.0.0",
|
||||
"http-errors": "2.0.0",
|
||||
"input-otp": "1.2.4",
|
||||
"input-otp": "1.4.1",
|
||||
"js-yaml": "4.1.0",
|
||||
"lucide-react": "0.447.0",
|
||||
"lucide-react": "0.469.0",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.0.1",
|
||||
"next-themes": "0.3.0",
|
||||
"next": "15.1.3",
|
||||
"next-themes": "0.4.4",
|
||||
"node-fetch": "3.3.2",
|
||||
"nodemailer": "6.9.15",
|
||||
"nodemailer": "6.9.16",
|
||||
"oslo": "1.2.1",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.0.0-rc.1",
|
||||
"react-dom": "19.0.0-rc.1",
|
||||
"react-hook-form": "7.53.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-hook-form": "7.54.2",
|
||||
"rebuild": "0.1.2",
|
||||
"semver": "7.6.3",
|
||||
"tailwind-merge": "2.5.3",
|
||||
"tailwind-merge": "2.6.0",
|
||||
"tailwindcss-animate": "1.0.7",
|
||||
"vaul": "1.1.1",
|
||||
"winston": "3.14.2",
|
||||
"vaul": "1.1.2",
|
||||
"winston": "3.17.0",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.18.0",
|
||||
"zod": "3.23.8",
|
||||
"zod": "3.24.1",
|
||||
"zod-validation-error": "3.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@dotenvx/dotenvx": "1.14.2",
|
||||
"@dotenvx/dotenvx": "1.32.0",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@types/better-sqlite3": "7.6.11",
|
||||
"@types/cookie-parser": "1.4.7",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"@types/cookie-parser": "1.4.8",
|
||||
"@types/cors": "2.8.17",
|
||||
"@types/express": "5.0.0",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "^20",
|
||||
"@types/nodemailer": "6.4.16",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"@types/node": "^22",
|
||||
"@types/nodemailer": "6.4.17",
|
||||
"@types/react": "19.0.2",
|
||||
"@types/react-dom": "19.0.2",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/ws": "8.5.13",
|
||||
"@types/yargs": "17.0.33",
|
||||
"drizzle-kit": "0.24.2",
|
||||
"esbuild": "0.20.1",
|
||||
"esbuild-node-externals": "1.13.0",
|
||||
"drizzle-kit": "0.30.1",
|
||||
"esbuild": "0.24.2",
|
||||
"esbuild-node-externals": "1.16.0",
|
||||
"postcss": "^8",
|
||||
"react-email": "3.0.2",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"react-email": "3.0.4",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tsc-alias": "1.8.10",
|
||||
"tsx": "4.19.1",
|
||||
"tsx": "4.19.2",
|
||||
"typescript": "^5",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1"
|
||||
"emblor": {
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import express, { Request, Response } from "express";
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import cookieParser from "cookie-parser";
|
||||
import config from "@server/config";
|
||||
|
|
174
server/config.ts
174
server/config.ts
|
@ -14,7 +14,10 @@ const portSchema = z.number().positive().gt(0).lte(65535);
|
|||
|
||||
const environmentSchema = z.object({
|
||||
app: z.object({
|
||||
base_url: z.string().url().transform((url) => url.toLowerCase()),
|
||||
base_url: z
|
||||
.string()
|
||||
.url()
|
||||
.transform((url) => url.toLowerCase()),
|
||||
log_level: z.enum(["debug", "info", "warn", "error"]),
|
||||
save_logs: z.boolean()
|
||||
}),
|
||||
|
@ -76,97 +79,102 @@ const environmentSchema = z.object({
|
|||
.optional()
|
||||
});
|
||||
|
||||
const loadConfig = (configPath: string) => {
|
||||
try {
|
||||
const yamlContent = fs.readFileSync(configPath, "utf8");
|
||||
const config = yaml.load(yamlContent);
|
||||
return config;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(
|
||||
`Error loading configuration file: ${error.message}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const configFilePath1 = path.join(APP_PATH, "config.yml");
|
||||
const configFilePath2 = path.join(APP_PATH, "config.yaml");
|
||||
|
||||
let environment: any;
|
||||
if (fs.existsSync(configFilePath1)) {
|
||||
environment = loadConfig(configFilePath1);
|
||||
} else if (fs.existsSync(configFilePath2)) {
|
||||
environment = loadConfig(configFilePath2);
|
||||
}
|
||||
if (!environment) {
|
||||
const exampleConfigPath = path.join(__DIRNAME, "config.example.yml");
|
||||
if (fs.existsSync(exampleConfigPath)) {
|
||||
export function getConfig() {
|
||||
const loadConfig = (configPath: string) => {
|
||||
try {
|
||||
const exampleConfigContent = fs.readFileSync(
|
||||
exampleConfigPath,
|
||||
"utf8"
|
||||
);
|
||||
fs.writeFileSync(configFilePath1, exampleConfigContent, "utf8");
|
||||
environment = loadConfig(configFilePath1);
|
||||
const yamlContent = fs.readFileSync(configPath, "utf8");
|
||||
const config = yaml.load(yamlContent);
|
||||
return config;
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(
|
||||
`Error creating configuration file from example: ${error.message}`
|
||||
`Error loading configuration file: ${error.message}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"No configuration file found and no example configuration available"
|
||||
);
|
||||
};
|
||||
|
||||
const configFilePath1 = path.join(APP_PATH, "config.yml");
|
||||
const configFilePath2 = path.join(APP_PATH, "config.yaml");
|
||||
|
||||
let environment: any;
|
||||
if (fs.existsSync(configFilePath1)) {
|
||||
environment = loadConfig(configFilePath1);
|
||||
} else if (fs.existsSync(configFilePath2)) {
|
||||
environment = loadConfig(configFilePath2);
|
||||
}
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("No configuration file found");
|
||||
}
|
||||
|
||||
const parsedConfig = environmentSchema.safeParse(environment);
|
||||
|
||||
if (!parsedConfig.success) {
|
||||
const errors = fromError(parsedConfig.error);
|
||||
throw new Error(`Invalid configuration file: ${errors}`);
|
||||
}
|
||||
|
||||
const packageJsonPath = path.join(__DIRNAME, "..", "package.json");
|
||||
let packageJson: any;
|
||||
if (fs.existsSync && fs.existsSync(packageJsonPath)) {
|
||||
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
|
||||
packageJson = JSON.parse(packageJsonContent);
|
||||
|
||||
if (packageJson.version) {
|
||||
process.env.APP_VERSION = packageJson.version;
|
||||
if (!environment) {
|
||||
const exampleConfigPath = path.join(__DIRNAME, "config.example.yml");
|
||||
if (fs.existsSync(exampleConfigPath)) {
|
||||
try {
|
||||
const exampleConfigContent = fs.readFileSync(
|
||||
exampleConfigPath,
|
||||
"utf8"
|
||||
);
|
||||
fs.writeFileSync(configFilePath1, exampleConfigContent, "utf8");
|
||||
environment = loadConfig(configFilePath1);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(
|
||||
`Error creating configuration file from example: ${error.message}`
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
"No configuration file found and no example configuration available"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("No configuration file found");
|
||||
}
|
||||
|
||||
const parsedConfig = environmentSchema.safeParse(environment);
|
||||
|
||||
if (!parsedConfig.success) {
|
||||
const errors = fromError(parsedConfig.error);
|
||||
throw new Error(`Invalid configuration file: ${errors}`);
|
||||
}
|
||||
|
||||
const packageJsonPath = path.join(__DIRNAME, "..", "package.json");
|
||||
let packageJson: any;
|
||||
if (fs.existsSync && fs.existsSync(packageJsonPath)) {
|
||||
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
|
||||
packageJson = JSON.parse(packageJsonContent);
|
||||
|
||||
if (packageJson.version) {
|
||||
process.env.APP_VERSION = packageJson.version;
|
||||
}
|
||||
}
|
||||
|
||||
process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
|
||||
process.env.SERVER_EXTERNAL_PORT =
|
||||
parsedConfig.data.server.external_port.toString();
|
||||
process.env.SERVER_INTERNAL_PORT =
|
||||
parsedConfig.data.server.internal_port.toString();
|
||||
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
|
||||
?.require_email_verification
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.SESSION_COOKIE_NAME =
|
||||
parsedConfig.data.server.session_cookie_name;
|
||||
process.env.RESOURCE_SESSION_COOKIE_NAME =
|
||||
parsedConfig.data.server.resource_session_cookie_name;
|
||||
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
|
||||
process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
|
||||
?.disable_signup_without_invite
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags
|
||||
?.disable_user_create_org
|
||||
? "true"
|
||||
: "false";
|
||||
|
||||
return parsedConfig.data;
|
||||
}
|
||||
|
||||
process.env.NEXT_PORT = parsedConfig.data.server.next_port.toString();
|
||||
process.env.SERVER_EXTERNAL_PORT =
|
||||
parsedConfig.data.server.external_port.toString();
|
||||
process.env.SERVER_INTERNAL_PORT =
|
||||
parsedConfig.data.server.internal_port.toString();
|
||||
process.env.FLAGS_EMAIL_VERIFICATION_REQUIRED = parsedConfig.data.flags
|
||||
?.require_email_verification
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.SESSION_COOKIE_NAME = parsedConfig.data.server.session_cookie_name;
|
||||
process.env.RESOURCE_SESSION_COOKIE_NAME =
|
||||
parsedConfig.data.server.resource_session_cookie_name;
|
||||
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
|
||||
process.env.DISABLE_SIGNUP_WITHOUT_INVITE = parsedConfig.data.flags
|
||||
?.disable_signup_without_invite
|
||||
? "true"
|
||||
: "false";
|
||||
process.env.DISABLE_USER_CREATE_ORG = parsedConfig.data.flags
|
||||
?.disable_user_create_org
|
||||
? "true"
|
||||
: "false";
|
||||
|
||||
export default parsedConfig.data;
|
||||
export default getConfig();
|
||||
|
|
|
@ -68,7 +68,7 @@ export const SendInviteLink = ({
|
|||
<Section className="text-center my-6">
|
||||
<Button
|
||||
href={inviteLink}
|
||||
className="rounded-lg bg-primary px-[12px] py-[9px] text-center font-semibold text-white cursor-pointer"
|
||||
className="rounded-lg bg-primary px-[12px] py-[9px] text-center font-semibold text-white cursor-pointer text-xl"
|
||||
>
|
||||
Accept invitation to {orgName}
|
||||
</Button>
|
||||
|
|
|
@ -8,8 +8,9 @@ export function createApiClient({ env }: { env: env }): AxiosInstance {
|
|||
return apiInstance;
|
||||
}
|
||||
|
||||
if (apiInstance) {
|
||||
return apiInstance
|
||||
if (typeof window === "undefined") {
|
||||
// @ts-ignore
|
||||
return;
|
||||
}
|
||||
|
||||
let baseURL;
|
||||
|
@ -45,7 +46,8 @@ export const internal = axios.create({
|
|||
baseURL: `http://localhost:${process.env.SERVER_EXTERNAL_PORT}/api/v1`,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRF-Token": "x-csrf-protection"
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -40,26 +40,6 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||
const { toast } = useToast();
|
||||
|
||||
const columns: ColumnDef<RoleRow>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Name
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: "Description"
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
|
@ -67,14 +47,9 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-end">
|
||||
<div>
|
||||
{roleRow.isAdmin && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0 opacity-0 cursor-default"
|
||||
>
|
||||
Placeholder
|
||||
</Button>
|
||||
<MoreHorizontal className="h-4 w-4 opacity-0" />
|
||||
)}
|
||||
{!roleRow.isAdmin && (
|
||||
<DropdownMenu>
|
||||
|
@ -107,6 +82,26 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
|||
</>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Name
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: "Description"
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -50,6 +50,64 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||
const { toast } = useToast();
|
||||
|
||||
const columns: ColumnDef<UserRow>[] = [
|
||||
{
|
||||
id: "dots",
|
||||
cell: ({ row }) => {
|
||||
const userRow = row.original;
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{userRow.isOwner && (
|
||||
<MoreHorizontal className="h-4 w-4 opacity-0" />
|
||||
)}
|
||||
{!userRow.isOwner && (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Open menu
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
className="block w-full"
|
||||
>
|
||||
Manage User
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{userRow.email !== user?.email && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(
|
||||
true
|
||||
);
|
||||
setSelectedUser(
|
||||
userRow
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
Remove User
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: ({ column }) => {
|
||||
|
@ -114,73 +172,27 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
|||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const userRow = row.original;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-end">
|
||||
{userRow.isOwner && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="opacity-0 cursor-default"
|
||||
>
|
||||
Placeholder
|
||||
<div className="flex items-center justify-end">
|
||||
{userRow.isOwner && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="opacity-0 cursor-default"
|
||||
>
|
||||
Placeholder
|
||||
</Button>
|
||||
)}
|
||||
{!userRow.isOwner && (
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
>
|
||||
<Button variant={"gray"} className="ml-2">
|
||||
Manage
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{!userRow.isOwner && (
|
||||
<>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Open menu
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
>
|
||||
Manage User
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
{userRow.email !== user?.email && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(
|
||||
true
|
||||
);
|
||||
setSelectedUser(
|
||||
userRow
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
Remove User
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
>
|
||||
<Button
|
||||
variant={"gray"}
|
||||
className="ml-2"
|
||||
>
|
||||
Manage
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -421,6 +421,7 @@ export default function ResourceAuthenticationPage() {
|
|||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>Roles</FormLabel>
|
||||
<FormControl>
|
||||
{/* @ts-ignore */}
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
|
@ -454,9 +455,9 @@ export default function ResourceAuthenticationPage() {
|
|||
tag: {
|
||||
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
|
||||
},
|
||||
input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
||||
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
||||
inlineTagsContainer:
|
||||
"bg-transparent"
|
||||
"bg-transparent p-2"
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
@ -476,6 +477,7 @@ export default function ResourceAuthenticationPage() {
|
|||
<FormItem className="flex flex-col items-start">
|
||||
<FormLabel>Users</FormLabel>
|
||||
<FormControl>
|
||||
{/* @ts-ignore */}
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
|
@ -509,9 +511,9 @@ export default function ResourceAuthenticationPage() {
|
|||
tag: {
|
||||
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
|
||||
},
|
||||
input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
||||
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
||||
inlineTagsContainer:
|
||||
"bg-transparent"
|
||||
"bg-transparent p-2"
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
@ -649,6 +651,7 @@ export default function ResourceAuthenticationPage() {
|
|||
Whitelisted Emails
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
{/* @ts-ignore */}
|
||||
<TagInput
|
||||
{...field}
|
||||
activeTagIndex={
|
||||
|
@ -691,9 +694,9 @@ export default function ResourceAuthenticationPage() {
|
|||
tag: {
|
||||
body: "bg-muted hover:bg-accent text-foreground py-2 px-3 rounded-full"
|
||||
},
|
||||
input: "border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
||||
input: "text-base md:text-sm border-none bg-transparent text-inherit placeholder:text-inherit shadow-none",
|
||||
inlineTagsContainer:
|
||||
"bg-transparent"
|
||||
"bg-transparent p-2"
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
|
|
@ -69,7 +69,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 bg-muted p-1 pl-3 rounded-md">
|
||||
<div className="flex items-center space-x-2 bg-muted p-1 pl-3 rounded-md lg:max-w-xl">
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
<a
|
||||
href={fullUrl}
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectValue
|
||||
} from "@/components/ui/select";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { AxiosResponse } from "axios";
|
||||
|
@ -24,7 +24,7 @@ import {
|
|||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import { CreateTargetResponse } from "@server/routers/target";
|
||||
import {
|
||||
|
@ -34,7 +34,7 @@ import {
|
|||
getPaginationRowModel,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
flexRender,
|
||||
flexRender
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Table,
|
||||
|
@ -42,7 +42,7 @@ import {
|
|||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableRow
|
||||
} from "@app/components/ui/table";
|
||||
import { useToast } from "@app/hooks/useToast";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
|
@ -59,9 +59,9 @@ const addTargetSchema = z.object({
|
|||
port: z
|
||||
.string()
|
||||
.refine((val) => !isNaN(Number(val)), {
|
||||
message: "Port must be a number",
|
||||
message: "Port must be a number"
|
||||
})
|
||||
.transform((val) => Number(val)),
|
||||
.transform((val) => Number(val))
|
||||
// protocol: z.string(),
|
||||
});
|
||||
|
||||
|
@ -99,16 +99,16 @@ export default function ReverseProxyTargets(props: {
|
|||
defaultValues: {
|
||||
ip: "",
|
||||
method: "http",
|
||||
port: "80",
|
||||
port: "80"
|
||||
// protocol: "TCP",
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTargets = async () => {
|
||||
try {
|
||||
const res = await api.get<AxiosResponse<ListTargetsResponse>>(
|
||||
`/resource/${params.resourceId}/targets`,
|
||||
`/resource/${params.resourceId}/targets`
|
||||
);
|
||||
|
||||
if (res.status === 200) {
|
||||
|
@ -121,8 +121,8 @@ export default function ReverseProxyTargets(props: {
|
|||
title: "Failed to fetch targets",
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
"An error occurred while fetching targets",
|
||||
),
|
||||
"An error occurred while fetching targets"
|
||||
)
|
||||
});
|
||||
} finally {
|
||||
setPageLoading(false);
|
||||
|
@ -133,7 +133,7 @@ export default function ReverseProxyTargets(props: {
|
|||
const fetchSite = async () => {
|
||||
try {
|
||||
const res = await api.get<AxiosResponse<GetSiteResponse>>(
|
||||
`/site/${resource.siteId}`,
|
||||
`/site/${resource.siteId}`
|
||||
);
|
||||
|
||||
if (res.status === 200) {
|
||||
|
@ -146,27 +146,28 @@ export default function ReverseProxyTargets(props: {
|
|||
title: "Failed to fetch resource",
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
"An error occurred while fetching resource",
|
||||
),
|
||||
"An error occurred while fetching resource"
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchSite();
|
||||
}, []);
|
||||
|
||||
async function addTarget(data: AddTargetFormValues) {
|
||||
// Check if target with same IP, port and method already exists
|
||||
const isDuplicate = targets.some(
|
||||
target => target.ip === data.ip &&
|
||||
target.port === data.port &&
|
||||
target.method === data.method
|
||||
(target) =>
|
||||
target.ip === data.ip &&
|
||||
target.port === data.port &&
|
||||
target.method === data.method
|
||||
);
|
||||
|
||||
if (isDuplicate) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Duplicate target",
|
||||
description: "A target with these settings already exists",
|
||||
description: "A target with these settings already exists"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -179,7 +180,7 @@ export default function ReverseProxyTargets(props: {
|
|||
toast({
|
||||
variant: "destructive",
|
||||
title: "Invalid target IP",
|
||||
description: "Target IP must be within the site subnet",
|
||||
description: "Target IP must be within the site subnet"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -190,7 +191,7 @@ export default function ReverseProxyTargets(props: {
|
|||
enabled: true,
|
||||
targetId: new Date().getTime(),
|
||||
new: true,
|
||||
resourceId: resource.resourceId,
|
||||
resourceId: resource.resourceId
|
||||
};
|
||||
|
||||
setTargets([...targets, newTarget]);
|
||||
|
@ -199,7 +200,7 @@ export default function ReverseProxyTargets(props: {
|
|||
|
||||
const removeTarget = (targetId: number) => {
|
||||
setTargets([
|
||||
...targets.filter((target) => target.targetId !== targetId),
|
||||
...targets.filter((target) => target.targetId !== targetId)
|
||||
]);
|
||||
|
||||
if (!targets.find((target) => target.targetId === targetId)?.new) {
|
||||
|
@ -212,8 +213,8 @@ export default function ReverseProxyTargets(props: {
|
|||
targets.map((target) =>
|
||||
target.targetId === targetId
|
||||
? { ...target, ...data, updated: true }
|
||||
: target,
|
||||
),
|
||||
: target
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -222,7 +223,7 @@ export default function ReverseProxyTargets(props: {
|
|||
setLoading(true);
|
||||
|
||||
const res = await api.post(`/resource/${params.resourceId}`, {
|
||||
ssl: sslEnabled,
|
||||
ssl: sslEnabled
|
||||
});
|
||||
|
||||
updateResource({ ssl: sslEnabled });
|
||||
|
@ -233,7 +234,7 @@ export default function ReverseProxyTargets(props: {
|
|||
port: target.port,
|
||||
// protocol: target.protocol,
|
||||
method: target.method,
|
||||
enabled: target.enabled,
|
||||
enabled: target.enabled
|
||||
};
|
||||
|
||||
if (target.new) {
|
||||
|
@ -244,7 +245,7 @@ export default function ReverseProxyTargets(props: {
|
|||
} else if (target.updated) {
|
||||
const res = await api.post(
|
||||
`/target/${target.targetId}`,
|
||||
data,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -253,23 +254,23 @@ export default function ReverseProxyTargets(props: {
|
|||
let res = {
|
||||
...t,
|
||||
new: false,
|
||||
updated: false,
|
||||
updated: false
|
||||
};
|
||||
return res;
|
||||
}),
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
for (const targetId of targetsToRemove) {
|
||||
await api.delete(`/target/${targetId}`);
|
||||
setTargets(
|
||||
targets.filter((target) => target.targetId !== targetId),
|
||||
targets.filter((target) => target.targetId !== targetId)
|
||||
);
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Resource updated",
|
||||
description: "Resource and targets updated successfully",
|
||||
description: "Resource and targets updated successfully"
|
||||
});
|
||||
|
||||
setTargetsToRemove([]);
|
||||
|
@ -280,8 +281,8 @@ export default function ReverseProxyTargets(props: {
|
|||
title: "Operation failed",
|
||||
description: formatAxiosError(
|
||||
err,
|
||||
"An error occurred during the save operation",
|
||||
),
|
||||
"An error occurred during the save operation"
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -299,13 +300,15 @@ export default function ReverseProxyTargets(props: {
|
|||
updateTarget(row.original.targetId, { method: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>{row.original.method}</SelectTrigger>
|
||||
<SelectTrigger className="min-w-[100px]">
|
||||
{row.original.method}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="http">http</SelectItem>
|
||||
<SelectItem value="https">https</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "ip",
|
||||
|
@ -313,13 +316,14 @@ export default function ReverseProxyTargets(props: {
|
|||
cell: ({ row }) => (
|
||||
<Input
|
||||
defaultValue={row.original.ip}
|
||||
className="min-w-[150px]"
|
||||
onBlur={(e) =>
|
||||
updateTarget(row.original.targetId, {
|
||||
ip: e.target.value,
|
||||
ip: e.target.value
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "port",
|
||||
|
@ -328,13 +332,14 @@ export default function ReverseProxyTargets(props: {
|
|||
<Input
|
||||
type="number"
|
||||
defaultValue={row.original.port}
|
||||
className="min-w-[100px]"
|
||||
onBlur={(e) =>
|
||||
updateTarget(row.original.targetId, {
|
||||
port: parseInt(e.target.value, 10),
|
||||
port: parseInt(e.target.value, 10)
|
||||
})
|
||||
}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
// {
|
||||
// accessorKey: "protocol",
|
||||
|
@ -364,7 +369,7 @@ export default function ReverseProxyTargets(props: {
|
|||
updateTarget(row.original.targetId, { enabled: val })
|
||||
}
|
||||
/>
|
||||
),
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
|
@ -387,8 +392,8 @@ export default function ReverseProxyTargets(props: {
|
|||
</Button>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
|
@ -397,7 +402,7 @@ export default function ReverseProxyTargets(props: {
|
|||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel()
|
||||
});
|
||||
|
||||
if (pageLoading) {
|
||||
|
@ -437,7 +442,7 @@ export default function ReverseProxyTargets(props: {
|
|||
<Form {...addTargetForm}>
|
||||
<form
|
||||
onSubmit={addTargetForm.handleSubmit(
|
||||
addTarget as any,
|
||||
addTarget as any
|
||||
)}
|
||||
className="space-y-4"
|
||||
>
|
||||
|
@ -452,11 +457,11 @@ export default function ReverseProxyTargets(props: {
|
|||
<Select
|
||||
{...field}
|
||||
onValueChange={(
|
||||
value,
|
||||
value
|
||||
) => {
|
||||
addTargetForm.setValue(
|
||||
"method",
|
||||
value,
|
||||
value
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
@ -585,10 +590,10 @@ export default function ReverseProxyTargets(props: {
|
|||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext(),
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
),
|
||||
)
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
|
@ -607,7 +612,7 @@ export default function ReverseProxyTargets(props: {
|
|||
cell.column
|
||||
.columnDef
|
||||
.cell,
|
||||
cell.getContext(),
|
||||
cell.getContext()
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
|
@ -644,36 +649,36 @@ export default function ReverseProxyTargets(props: {
|
|||
|
||||
function isIPInSubnet(subnet: string, ip: string): boolean {
|
||||
// Split subnet into IP and mask parts
|
||||
const [subnetIP, maskBits] = subnet.split('/');
|
||||
const [subnetIP, maskBits] = subnet.split("/");
|
||||
const mask = parseInt(maskBits);
|
||||
|
||||
|
||||
if (mask < 0 || mask > 32) {
|
||||
throw new Error('Invalid subnet mask. Must be between 0 and 32.');
|
||||
throw new Error("Invalid subnet mask. Must be between 0 and 32.");
|
||||
}
|
||||
|
||||
// Convert IP addresses to binary numbers
|
||||
const subnetNum = ipToNumber(subnetIP);
|
||||
const ipNum = ipToNumber(ip);
|
||||
|
||||
|
||||
// Calculate subnet mask
|
||||
const maskNum = mask === 32 ? -1 : ~((1 << (32 - mask)) - 1);
|
||||
|
||||
|
||||
// Check if the IP is in the subnet
|
||||
return (subnetNum & maskNum) === (ipNum & maskNum);
|
||||
}
|
||||
|
||||
function ipToNumber(ip: string): number {
|
||||
// Validate IP address format
|
||||
const parts = ip.split('.');
|
||||
const parts = ip.split(".");
|
||||
if (parts.length !== 4) {
|
||||
throw new Error('Invalid IP address format');
|
||||
throw new Error("Invalid IP address format");
|
||||
}
|
||||
|
||||
|
||||
// Convert IP octets to 32-bit number
|
||||
return parts.reduce((num, octet) => {
|
||||
const oct = parseInt(octet);
|
||||
if (isNaN(oct) || oct < 0 || oct > 255) {
|
||||
throw new Error('Invalid IP address octet');
|
||||
throw new Error("Invalid IP address octet");
|
||||
}
|
||||
return (num << 8) + oct;
|
||||
}, 0);
|
||||
|
|
|
@ -123,7 +123,7 @@ export default async function ResourceLayout(props: ResourceLayoutProps) {
|
|||
<OrgProvider org={org}>
|
||||
<ResourceProvider resource={resource} authInfo={authInfo}>
|
||||
<SidebarSettings sidebarNavItems={sidebarNavItems}>
|
||||
<div className="mb-8 lg:max-w-2xl">
|
||||
<div className="mb-8">
|
||||
<ResourceInfoBox />
|
||||
</div>
|
||||
{children}
|
||||
|
|
|
@ -74,6 +74,43 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
|||
};
|
||||
|
||||
const columns: ColumnDef<ResourceRow>[] = [
|
||||
{
|
||||
accessorKey: "dots",
|
||||
header: "",
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Link
|
||||
className="block w-full"
|
||||
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
||||
>
|
||||
View settings
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedResource(resourceRow);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
|
@ -214,55 +251,18 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
|||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const resourceRow = row.original;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Open menu
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Link
|
||||
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
||||
>
|
||||
View settings
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedResource(resourceRow);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
Delete
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
||||
>
|
||||
<Button variant={"gray"} className="ml-2">
|
||||
Edit
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
<div className="flex items-center justify-end">
|
||||
<Link
|
||||
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.id}`}
|
||||
>
|
||||
<Button variant={"gray"} className="ml-2">
|
||||
Edit
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ import { Checkbox } from "@app/components/ui/checkbox";
|
|||
import { GenerateAccessTokenResponse } from "@server/routers/accessToken";
|
||||
import { constructShareLink } from "@app/lib/shareLinks";
|
||||
import { ShareLinkRow } from "./ShareLinksTable";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
|
||||
|
||||
type FormProps = {
|
||||
open: boolean;
|
||||
|
@ -449,23 +449,23 @@ export default function CreateShareLinkForm({
|
|||
{link && (
|
||||
<div className="max-w-md space-y-4">
|
||||
<p>
|
||||
You will only be able to see this link once.
|
||||
Make sure to copy it.
|
||||
You will only be able to see this link
|
||||
once. Make sure to copy it.
|
||||
</p>
|
||||
<p>
|
||||
Anyone with this link can access the
|
||||
resource. Share it with care.
|
||||
</p>
|
||||
|
||||
<div className="w-64 h-64 mx-auto flex items-center justify-center">
|
||||
<QRCodeSVG
|
||||
value={link}
|
||||
size={256}
|
||||
/>
|
||||
<div className="h-[250px] w-full mx-auto flex items-center justify-center">
|
||||
<QRCodeCanvas value={link} size={200} />
|
||||
</div>
|
||||
|
||||
<div className="mx-auto">
|
||||
<CopyTextBox text={link} wrapText={false} />
|
||||
<CopyTextBox
|
||||
text={link}
|
||||
wrapText={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
@ -473,8 +473,8 @@ export default function CreateShareLinkForm({
|
|||
</CredenzaBody>
|
||||
<CredenzaFooter>
|
||||
<Button
|
||||
type="submit"
|
||||
form="share-link-form"
|
||||
type="button"
|
||||
onClick={form.handleSubmit(onSubmit)}
|
||||
loading={loading}
|
||||
disabled={link !== null || loading}
|
||||
>
|
||||
|
|
|
@ -86,6 +86,48 @@ export default function ShareLinksTable({
|
|||
}
|
||||
|
||||
const columns: ColumnDef<ShareLinkRow>[] = [
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const resourceRow = row.original;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Open menu
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<button
|
||||
onClick={() =>
|
||||
deleteSharelink(
|
||||
resourceRow.accessTokenId
|
||||
)
|
||||
}
|
||||
className="text-red-500"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "resourceName",
|
||||
header: ({ column }) => {
|
||||
|
@ -236,48 +278,6 @@ export default function ShareLinksTable({
|
|||
}
|
||||
return "Never";
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const resourceRow = row.original;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<span className="sr-only">
|
||||
Open menu
|
||||
</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<button
|
||||
onClick={() =>
|
||||
deleteSharelink(
|
||||
resourceRow.accessTokenId
|
||||
)
|
||||
}
|
||||
className="text-red-500"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<Link href="../../">Sites</Link>
|
||||
<Link href="../">Sites</Link>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
<BreadcrumbItem>
|
||||
|
|
|
@ -71,10 +71,48 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
|||
setIsDeleteModalOpen(false);
|
||||
|
||||
const newRows = rows.filter((row) => row.id !== siteId);
|
||||
|
||||
setRows(newRows);
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnDef<SiteRow>[] = [
|
||||
{
|
||||
id: "dots",
|
||||
cell: ({ row }) => {
|
||||
const siteRow = row.original;
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Link
|
||||
className="block w-full"
|
||||
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
||||
>
|
||||
View settings
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedSite(siteRow);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
|
@ -91,6 +129,41 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
|||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "online",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Online
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
|
||||
if (originalRow.online) {
|
||||
return (
|
||||
<span className="text-green-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>Online</span>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="text-neutral-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||
<span>Offline</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "nice",
|
||||
header: ({ column }) => {
|
||||
|
@ -174,75 +247,12 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "online",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Online
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
|
||||
if (originalRow.online) {
|
||||
return (
|
||||
<span className="text-green-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>Online</span>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="text-gray-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||
<span>Offline</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const siteRow = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem>
|
||||
<Link
|
||||
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
||||
>
|
||||
View settings
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedSite(siteRow);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">Delete</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
||||
>
|
||||
|
|
|
@ -6,11 +6,11 @@
|
|||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 5.0% 10.0%;
|
||||
--foreground: 20 0.0% 10.0%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 20 5.0% 10.0%;
|
||||
--card-foreground: 20 0.0% 10.0%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 20 5.0% 10.0%;
|
||||
--popover-foreground: 20 0.0% 10.0%;
|
||||
--primary: 24.6 95% 53.1%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
--secondary: 60 4.8% 95.9%;
|
||||
|
@ -33,11 +33,11 @@
|
|||
}
|
||||
|
||||
.dark {
|
||||
--background: 20 5.0% 10.0%;
|
||||
--background: 20 0.0% 10.0%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
--card: 20 5.0% 10.0%;
|
||||
--card: 20 0.0% 10.0%;
|
||||
--card-foreground: 60 9.1% 97.8%;
|
||||
--popover: 20 5.0% 10.0%;
|
||||
--popover: 20 0.0% 10.0%;
|
||||
--popover-foreground: 60 9.1% 97.8%;
|
||||
--primary: 20.5 90.2% 48.2%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
CardTitle,
|
||||
} from "@app/components/ui/card";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { XCircle } from "lucide-react";
|
||||
|
@ -20,7 +20,7 @@ type InviteStatusCardProps = {
|
|||
|
||||
export default function InviteStatusCard({
|
||||
type,
|
||||
token
|
||||
token,
|
||||
}: InviteStatusCardProps) {
|
||||
const router = useRouter();
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogTrigger
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Drawer,
|
||||
|
@ -22,8 +22,17 @@ import {
|
|||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
DrawerTrigger
|
||||
} from "@/components/ui/drawer";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger
|
||||
} from "./ui/sheet";
|
||||
|
||||
interface BaseProps {
|
||||
children: React.ReactNode;
|
||||
|
@ -43,14 +52,17 @@ const desktop = "(min-width: 768px)";
|
|||
|
||||
const Credenza = ({ children, ...props }: RootCredenzaProps) => {
|
||||
const isDesktop = useMediaQuery(desktop);
|
||||
const Credenza = isDesktop ? Dialog : Drawer;
|
||||
// const isDesktop = true;
|
||||
const Credenza = isDesktop ? Dialog : Sheet;
|
||||
|
||||
return <Credenza {...props}>{children}</Credenza>;
|
||||
};
|
||||
|
||||
const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
|
||||
const isDesktop = useMediaQuery(desktop);
|
||||
const CredenzaTrigger = isDesktop ? DialogTrigger : DrawerTrigger;
|
||||
// const isDesktop = true;
|
||||
|
||||
const CredenzaTrigger = isDesktop ? DialogTrigger : SheetTrigger;
|
||||
|
||||
return (
|
||||
<CredenzaTrigger className={className} {...props}>
|
||||
|
@ -61,10 +73,12 @@ const CredenzaTrigger = ({ className, children, ...props }: CredenzaProps) => {
|
|||
|
||||
const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
|
||||
const isDesktop = useMediaQuery(desktop);
|
||||
// const isDesktop = true;
|
||||
|
||||
const CredenzaClose = isDesktop ? DialogClose : DrawerClose;
|
||||
|
||||
return (
|
||||
<CredenzaClose className={className} {...props}>
|
||||
<CredenzaClose className={cn("mb-3 md:mb-0", className)} {...props}>
|
||||
{children}
|
||||
</CredenzaClose>
|
||||
);
|
||||
|
@ -72,10 +86,16 @@ const CredenzaClose = ({ className, children, ...props }: CredenzaProps) => {
|
|||
|
||||
const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
|
||||
const isDesktop = useMediaQuery(desktop);
|
||||
const CredenzaContent = isDesktop ? DialogContent : DrawerContent;
|
||||
// const isDesktop = true;
|
||||
|
||||
const CredenzaContent = isDesktop ? DialogContent : SheetContent;
|
||||
|
||||
return (
|
||||
<CredenzaContent className={className} {...props}>
|
||||
<CredenzaContent
|
||||
className={cn("overflow-y-auto max-h-screen", className)}
|
||||
{...props}
|
||||
side={"bottom"}
|
||||
>
|
||||
{children}
|
||||
</CredenzaContent>
|
||||
);
|
||||
|
@ -87,9 +107,11 @@ const CredenzaDescription = ({
|
|||
...props
|
||||
}: CredenzaProps) => {
|
||||
const isDesktop = useMediaQuery(desktop);
|
||||
// const isDesktop = true;
|
||||
|
||||
const CredenzaDescription = isDesktop
|
||||
? DialogDescription
|
||||
: DrawerDescription;
|
||||
: SheetDescription;
|
||||
|
||||
return (
|
||||
<CredenzaDescription className={className} {...props}>
|
||||
|
@ -100,7 +122,9 @@ const CredenzaDescription = ({
|
|||
|
||||
const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
|
||||
const isDesktop = useMediaQuery(desktop);
|
||||
const CredenzaHeader = isDesktop ? DialogHeader : DrawerHeader;
|
||||
// const isDesktop = true;
|
||||
|
||||
const CredenzaHeader = isDesktop ? DialogHeader : SheetHeader;
|
||||
|
||||
return (
|
||||
<CredenzaHeader className={className} {...props}>
|
||||
|
@ -111,7 +135,9 @@ const CredenzaHeader = ({ className, children, ...props }: CredenzaProps) => {
|
|||
|
||||
const CredenzaTitle = ({ className, children, ...props }: CredenzaProps) => {
|
||||
const isDesktop = useMediaQuery(desktop);
|
||||
const CredenzaTitle = isDesktop ? DialogTitle : DrawerTitle;
|
||||
// const isDesktop = true;
|
||||
|
||||
const CredenzaTitle = isDesktop ? DialogTitle : SheetTitle;
|
||||
|
||||
return (
|
||||
<CredenzaTitle className={className} {...props}>
|
||||
|
@ -121,8 +147,14 @@ const CredenzaTitle = ({ className, children, ...props }: CredenzaProps) => {
|
|||
};
|
||||
|
||||
const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
|
||||
// return (
|
||||
// <div className={cn("px-4 md:px-0 mb-4", className)} {...props}>
|
||||
// {children}
|
||||
// </div>
|
||||
// );
|
||||
|
||||
return (
|
||||
<div className={cn("px-4 md:px-0 mb-4", className)} {...props}>
|
||||
<div className={cn("px-0 mb-4", className)} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
@ -130,7 +162,9 @@ const CredenzaBody = ({ className, children, ...props }: CredenzaProps) => {
|
|||
|
||||
const CredenzaFooter = ({ className, children, ...props }: CredenzaProps) => {
|
||||
const isDesktop = useMediaQuery(desktop);
|
||||
const CredenzaFooter = isDesktop ? DialogFooter : DrawerFooter;
|
||||
// const isDesktop = true;
|
||||
|
||||
const CredenzaFooter = isDesktop ? DialogFooter : SheetFooter;
|
||||
|
||||
return (
|
||||
<CredenzaFooter className={className} {...props}>
|
||||
|
@ -148,5 +182,5 @@ export {
|
|||
CredenzaHeader,
|
||||
CredenzaTitle,
|
||||
CredenzaBody,
|
||||
CredenzaFooter,
|
||||
CredenzaFooter
|
||||
};
|
||||
|
|
|
@ -38,7 +38,7 @@ import {
|
|||
import { useToast } from "@app/hooks/useToast";
|
||||
import { formatAxiosError } from "@app/lib/utils";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import { QRCodeCanvas, QRCodeSVG } from "qrcode.react";
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
|
||||
const enableSchema = z.object({
|
||||
|
@ -221,15 +221,10 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
|
|||
Scan this QR code with your authenticator app or
|
||||
enter the secret key manually:
|
||||
</p>
|
||||
<div className="w-64 h-64 mx-auto flex items-center justify-center">
|
||||
<QRCodeSVG value={secretUri} size={256} />
|
||||
</div>
|
||||
<div className="max-w-md mx-auto">
|
||||
<CopyTextBox
|
||||
text={secretKey}
|
||||
wrapText={false}
|
||||
/>
|
||||
<div className="h-[250px] mx-auto flex items-center justify-center">
|
||||
<QRCodeCanvas value={secretUri} size={200} />
|
||||
</div>
|
||||
<CopyTextBox text={secretUri} wrapText={false} />
|
||||
|
||||
<Form {...confirmForm}>
|
||||
<form
|
||||
|
@ -288,10 +283,16 @@ export default function Enable2FaForm({ open, setOpen }: Enable2FaProps) {
|
|||
<CredenzaFooter>
|
||||
{(step === 1 || step === 2) && (
|
||||
<Button
|
||||
type="submit"
|
||||
form="form"
|
||||
type="button"
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
onClick={() => {
|
||||
if (step === 1) {
|
||||
enableForm.handleSubmit(request2fa)();
|
||||
} else {
|
||||
confirmForm.handleSubmit(confirm2fa)();
|
||||
}
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
|
|
|
@ -21,9 +21,9 @@ export function SidebarSettings({
|
|||
limitWidth,
|
||||
}: SideBarSettingsProps) {
|
||||
return (
|
||||
<div className="space-y-8 0 pb-16k">
|
||||
<div className="space-y-8 pb-16k">
|
||||
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-32 lg:space-y-0">
|
||||
<aside className="-mx-4 lg:w-1/5">
|
||||
<aside className="lg:w-1/5">
|
||||
<SidebarNav items={sidebarNavItems} disabled={disabled} />
|
||||
</aside>
|
||||
<div className={`flex-1 ${limitWidth ? "lg:max-w-2xl" : ""}`}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useParams, usePathname, useRouter } from "next/navigation";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
@ -35,6 +35,12 @@ export function SidebarNav({
|
|||
const resourceId = params.resourceId as string;
|
||||
const userId = params.userId as string;
|
||||
|
||||
const [selectedValue, setSelectedValue] = React.useState<string>(getSelectedValue());
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedValue(getSelectedValue());
|
||||
}, [usePathname()]);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const handleSelectChange = (value: string) => {
|
||||
|
@ -58,9 +64,10 @@ export function SidebarNav({
|
|||
|
||||
return (
|
||||
<div>
|
||||
<div className="block lg:hidden px-4">
|
||||
<div className="block lg:hidden">
|
||||
<Select
|
||||
defaultValue={getSelectedValue()}
|
||||
defaultValue={selectedValue}
|
||||
value={selectedValue}
|
||||
onValueChange={handleSelectChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
|
|
|
@ -46,7 +46,7 @@ const CommandInput = React.forwardRef<
|
|||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-base md:text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
|||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/30 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
@ -118,5 +118,5 @@ export {
|
|||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogDescription
|
||||
};
|
||||
|
|
|
@ -1,118 +1,118 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import * as React from "react"
|
||||
import { Drawer as DrawerPrimitive } from "vaul"
|
||||
import * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||
<DrawerPrimitive.Root
|
||||
shouldScaleBackground={shouldScaleBackground}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
Drawer.displayName = "Drawer"
|
||||
<DrawerPrimitive.Root
|
||||
shouldScaleBackground={shouldScaleBackground}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
Drawer.displayName = "Drawer";
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger;
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal
|
||||
const DrawerPortal = DrawerPrimitive.Portal;
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close
|
||||
const DrawerClose = DrawerPrimitive.Close;
|
||||
|
||||
const DrawerOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
|
||||
|
||||
const DrawerContent = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
))
|
||||
DrawerContent.displayName = "DrawerContent"
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
));
|
||||
DrawerContent.displayName = "DrawerContent";
|
||||
|
||||
const DrawerHeader = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerHeader.displayName = "DrawerHeader"
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DrawerHeader.displayName = "DrawerHeader";
|
||||
|
||||
const DrawerFooter = ({
|
||||
className,
|
||||
...props
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DrawerFooter.displayName = "DrawerFooter"
|
||||
<div
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DrawerFooter.displayName = "DrawerFooter";
|
||||
|
||||
const DrawerTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
|
||||
|
||||
const DrawerDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
}
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription
|
||||
};
|
||||
|
|
|
@ -41,7 +41,7 @@ const InputOTPSlot = React.forwardRef<
|
|||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
"relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-base md:text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
||||
className
|
||||
)}
|
||||
|
|
|
@ -15,7 +15,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
<input
|
||||
type={showPassword ? "text" : "password"}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
@ -39,7 +39,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-9 w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-base md:file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
|
|
@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
|
|||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
|
140
src/components/ui/sheet.tsx
Normal file
140
src/components/ui/sheet.tsx
Normal file
|
@ -0,0 +1,140 @@
|
|||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = "SheetHeader"
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = "SheetFooter"
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
|
@ -25,7 +25,7 @@ const ToastViewport = React.forwardRef<
|
|||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-3 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
|
|
@ -6,7 +6,7 @@ import * as React from "react";
|
|||
import type { ToastActionElement, ToastProps } from "@/components/ui/toast";
|
||||
|
||||
const TOAST_LIMIT = 3;
|
||||
const TOAST_REMOVE_DELAY = 5 * 1000;
|
||||
const TOAST_REMOVE_DELAY = 1 * 1000;
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||
|
||||
type ThemeProviderProps = React.ComponentProps<typeof NextThemesProvider>;
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue