add toggle resource visibility closes #442

This commit is contained in:
miloschwartz 2025-03-31 10:10:28 -04:00
parent fbd78ab842
commit e7ca7fe89c
No known key found for this signature in database
12 changed files with 121 additions and 16 deletions

View file

@ -7,7 +7,7 @@ RUN npm ci
COPY . .
RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/schema.ts --out init
RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/schemas/ --out init
RUN npm run build

View file

@ -4,10 +4,10 @@ import path from "path";
export default defineConfig({
dialect: "sqlite",
schema: path.join("server", "db", "schema.ts"),
schema: path.join("server", "db", "schemas"),
out: path.join("server", "migrations"),
verbose: true,
dbCredentials: {
url: path.join(APP_PATH, "db", "db.sqlite"),
},
url: path.join(APP_PATH, "db", "db.sqlite")
}
});

View file

@ -14,7 +14,7 @@ import { logIncomingMiddleware } from "./middlewares/logIncoming";
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
import helmet from "helmet";
const dev = process.env.ENVIRONMENT !== "prod";
const dev = config.isDev;
const externalPort = config.getRawConfig().server.external_port;
export function createApiServer() {

View file

@ -76,7 +76,8 @@ export const resources = sqliteTable("resources", {
isBaseDomain: integer("isBaseDomain", { mode: "boolean" }),
applyRules: integer("applyRules", { mode: "boolean" })
.notNull()
.default(false)
.default(false),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true)
});
export const targets = sqliteTable("targets", {

View file

@ -163,6 +163,8 @@ export class Config {
supporterHiddenUntil: number | null = null;
isDev: boolean = process.env.ENVIRONMENT !== "prod";
constructor() {
this.loadConfig();
}
@ -245,7 +247,9 @@ export class Config {
: "false";
process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url;
this.checkSupporterKey();
if (!this.isDev) {
this.checkSupporterKey();
}
this.rawConfig = parsedConfig.data;
}

View file

@ -66,7 +66,8 @@ function queryResources(
whitelist: resources.emailWhitelistEnabled,
http: resources.http,
protocol: resources.protocol,
proxyPort: resources.proxyPort
proxyPort: resources.proxyPort,
enabled: resources.enabled
})
.from(resources)
.leftJoin(sites, eq(resources.siteId, sites.siteId))
@ -99,7 +100,8 @@ function queryResources(
whitelist: resources.emailWhitelistEnabled,
http: resources.http,
protocol: resources.protocol,
proxyPort: resources.proxyPort
proxyPort: resources.proxyPort,
enabled: resources.enabled
})
.from(resources)
.leftJoin(sites, eq(resources.siteId, sites.siteId))

View file

@ -39,7 +39,8 @@ const updateHttpResourceBodySchema = z
emailWhitelistEnabled: z.boolean().optional(),
isBaseDomain: z.boolean().optional(),
applyRules: z.boolean().optional(),
domainId: z.string().optional()
domainId: z.string().optional(),
enabled: z.boolean().optional()
})
.strict()
.refine((data) => Object.keys(data).length > 0, {
@ -73,7 +74,8 @@ export type UpdateResourceResponse = Resource;
const updateRawResourceBodySchema = z
.object({
name: z.string().min(1).max(255).optional(),
proxyPort: z.number().int().min(1).max(65535).optional()
proxyPort: z.number().int().min(1).max(65535).optional(),
enabled: z.boolean().optional()
})
.strict()
.refine((data) => Object.keys(data).length > 0, {

View file

@ -39,7 +39,8 @@ export async function traefikConfigProvider(
// Org fields
org: {
orgId: orgs.orgId
}
},
enabled: resources.enabled
})
.from(resources)
.innerJoin(sites, eq(sites.siteId, resources.siteId))
@ -136,6 +137,10 @@ export async function traefikConfigProvider(
const serviceName = `${resource.resourceId}-service`;
const fullDomain = `${resource.fullDomain}`;
if (!resource.enabled) {
continue;
}
if (resource.http) {
if (!resource.domainId) {
continue;

View file

@ -30,6 +30,9 @@ import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import CopyToClipboard from "@app/components/CopyToClipboard";
import { Switch } from "@app/components/ui/switch";
import { AxiosResponse } from "axios";
import { UpdateResourceResponse } from "@server/routers/resource";
export type ResourceRow = {
id: number;
@ -42,6 +45,7 @@ export type ResourceRow = {
http: boolean;
protocol: string;
proxyPort: number | null;
enabled: boolean;
};
type ResourcesTableProps = {
@ -75,6 +79,26 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
});
};
async function toggleResourceEnabled(val: boolean, resourceId: number) {
const res = await api
.post<AxiosResponse<UpdateResourceResponse>>(
`resource/${resourceId}`,
{
enabled: val
}
)
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to toggle resource",
description: formatAxiosError(
e,
"An error occurred while updating the resource"
)
});
});
}
const columns: ColumnDef<ResourceRow>[] = [
{
accessorKey: "dots",
@ -224,6 +248,18 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
);
}
},
{
accessorKey: "enabled",
header: "Enabled",
cell: ({ row }) => (
<Switch
defaultChecked={row.original.enabled}
onCheckedChange={(val) =>
toggleResourceEnabled(val, row.original.id)
}
/>
)
},
{
id: "actions",
cell: ({ row }) => {

View file

@ -12,6 +12,7 @@ import {
InfoSectionTitle
} from "@app/components/InfoSection";
import Link from "next/link";
import { Switch } from "@app/components/ui/switch";
type ResourceInfoBoxType = {};
@ -27,7 +28,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
Resource Information
</AlertTitle>
<AlertDescription className="mt-4">
<InfoSections cols={3}>
<InfoSections cols={4}>
{resource.http ? (
<>
<InfoSection>
@ -88,6 +89,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
</InfoSection>
</>
)}
<InfoSection>
<InfoSectionTitle>Visibilty</InfoSectionTitle>
<InfoSectionContent>
<span>{resource.enabled ? "Enabled" : "Disabled"}</span>
</InfoSectionContent>
</InfoSection>
</InfoSections>
</AlertDescription>
</Alert>

View file

@ -60,7 +60,11 @@ import {
SelectTrigger,
SelectValue
} from "@app/components/ui/select";
import { UpdateResourceResponse } from "@server/routers/resource";
import {
UpdateResourceResponse,
updateResourceRule
} from "@server/routers/resource";
import { SwitchInput } from "@app/components/SwitchInput";
const GeneralFormSchema = z
.object({
@ -275,9 +279,52 @@ export default function GeneralForm() {
setTransferLoading(false);
}
async function toggleResourceEnabled(val: boolean) {
const res = await api
.post<AxiosResponse<UpdateResourceResponse>>(
`resource/${resource.resourceId}`,
{
enabled: val
}
)
.catch((e) => {
toast({
variant: "destructive",
title: "Failed to toggle resource",
description: formatAxiosError(
e,
"An error occurred while updating the resource"
)
});
});
updateResource({
enabled: val
});
}
return (
!loadingPage && (
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>Visibility</SettingsSectionTitle>
<SettingsSectionDescription>
Completely enable or disable resource visibility
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<SwitchInput
id="enable-resource"
label="Enable Resource"
defaultChecked={resource.enabled}
onCheckedChange={async (val) => {
await toggleResourceEnabled(val);
}}
/>
</SettingsSectionBody>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>

View file

@ -63,7 +63,8 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
resource.pincodeId !== null ||
resource.whitelist
? "protected"
: "not_protected"
: "not_protected",
enabled: resource.enabled
};
});