chore: update package dependencies and enhance AppShell component with new navigation items and tooltip support. Refactor API client for improved error handling and response parsing. Expand type definitions for modules, entries, and communities, and enhance UI components with better structure and accessibility features.
CI / changes (push) Successful in 5s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 20s
CI / bird2 (push) Successful in 16s
CI / docker-images (deploy/docker/bird2/Dockerfile, , evobgp-bird2) (push) Successful in 40s
CI / docker-images (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m6s
CI / docker-images (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m1s
CI / docker-images (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 59s
CI / docker-images (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m23s
CI / docker-images (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m31s
CI / docker-images (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m27s
CI / docker-images (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m12s
CI / docker-images (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m25s
CI / docker-images (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m24s
CI / docker-images (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m31s
CI / changes (push) Successful in 5s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 20s
CI / bird2 (push) Successful in 16s
CI / docker-images (deploy/docker/bird2/Dockerfile, , evobgp-bird2) (push) Successful in 40s
CI / docker-images (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m6s
CI / docker-images (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m1s
CI / docker-images (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 59s
CI / docker-images (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m23s
CI / docker-images (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Successful in 1m31s
CI / docker-images (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Successful in 1m27s
CI / docker-images (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Successful in 1m12s
CI / docker-images (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Successful in 1m25s
CI / docker-images (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Successful in 1m24s
CI / docker-images (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Successful in 1m31s
This commit is contained in:
+56
-16
@@ -3,44 +3,84 @@ import { browser } from '$app/environment';
|
||||
export const TOKEN_STORAGE_KEY = 'evobgp_api_token';
|
||||
|
||||
export type Problem = {
|
||||
type?: string;
|
||||
title?: string;
|
||||
status?: number;
|
||||
detail?: string;
|
||||
};
|
||||
|
||||
function mergeHeaders(init?: RequestInit): Headers {
|
||||
function getToken(): string | null {
|
||||
if (!browser) return null;
|
||||
return localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
}
|
||||
|
||||
function mergeHeaders(init?: RequestInit, extraHeaders?: Record<string, string>): Headers {
|
||||
const h = new Headers(init?.headers);
|
||||
if (!h.has('Accept')) {
|
||||
h.set('Accept', 'application/json');
|
||||
}
|
||||
if (browser) {
|
||||
const t = localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
if (t && !h.has('Authorization')) {
|
||||
h.set('Authorization', `Bearer ${t}`);
|
||||
if (!h.has('Accept')) h.set('Accept', 'application/json');
|
||||
const t = getToken();
|
||||
if (t && !h.has('Authorization')) h.set('Authorization', `Bearer ${t}`);
|
||||
if (extraHeaders) {
|
||||
for (const [k, v] of Object.entries(extraHeaders)) {
|
||||
if (!h.has(k)) h.set(k, v);
|
||||
}
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
export async function apiFetch(path: string, init?: RequestInit): Promise<Response> {
|
||||
if (!browser) {
|
||||
throw new Error('API is only available in the browser');
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public readonly status: number,
|
||||
message: string,
|
||||
public readonly problem?: Problem
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiFetch(path: string, init?: RequestInit): Promise<Response> {
|
||||
if (!browser) throw new Error('API is only available in the browser');
|
||||
return fetch(path, { ...init, headers: mergeHeaders(init) });
|
||||
}
|
||||
|
||||
/** GET / DELETE без тела */
|
||||
export async function apiJSON<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await apiFetch(path, init);
|
||||
return parseResponse<T>(res);
|
||||
}
|
||||
|
||||
/** POST / PATCH / PUT с JSON-телом и автоматическим Idempotency-Key */
|
||||
export async function apiMutate<T = void>(
|
||||
path: string,
|
||||
method: 'POST' | 'PATCH' | 'PUT' | 'DELETE',
|
||||
body?: unknown,
|
||||
opts?: { idempotent?: boolean }
|
||||
): Promise<T> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
||||
if (opts?.idempotent !== false) {
|
||||
headers['Idempotency-Key'] = crypto.randomUUID();
|
||||
}
|
||||
const res = await fetch(path, {
|
||||
method,
|
||||
headers: mergeHeaders({ headers }, headers),
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined
|
||||
});
|
||||
return parseResponse<T>(res);
|
||||
}
|
||||
|
||||
async function parseResponse<T>(res: Response): Promise<T> {
|
||||
if (res.status === 204 || res.status === 205) return undefined as T;
|
||||
const text = await res.text();
|
||||
if (!res.ok) {
|
||||
let detail = text;
|
||||
let problem: Problem | undefined;
|
||||
let detail = `HTTP ${res.status}`;
|
||||
try {
|
||||
const j = JSON.parse(text) as Problem;
|
||||
if (j?.detail) detail = j.detail;
|
||||
problem = JSON.parse(text) as Problem;
|
||||
detail = problem.detail ?? problem.title ?? detail;
|
||||
} catch {
|
||||
/* plain text */
|
||||
if (text) detail = text;
|
||||
}
|
||||
throw new Error(detail || `HTTP ${res.status}`);
|
||||
throw new ApiError(res.status, detail, problem);
|
||||
}
|
||||
if (!text) return undefined as T;
|
||||
return JSON.parse(text) as T;
|
||||
|
||||
+161
-45
@@ -1,21 +1,152 @@
|
||||
export type ModuleRow = {
|
||||
id: string;
|
||||
type: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
refresh_interval_sec: number;
|
||||
cron_expr: string;
|
||||
default_community_id: string | null;
|
||||
doh_profile_id: string | null;
|
||||
};
|
||||
|
||||
export type ModulesResponse = {
|
||||
items: ModuleRow[];
|
||||
// ---- Pagination ----
|
||||
export type Page<T> = {
|
||||
items: T[];
|
||||
next_cursor: string | null;
|
||||
has_more: boolean;
|
||||
};
|
||||
|
||||
// ---- Modules ----
|
||||
export type ModuleType = 'AS_PREFIXES' | 'CDN_CIDRS' | 'DOMAINS' | 'IP_RANGES';
|
||||
|
||||
export type ModuleRow = {
|
||||
id: string;
|
||||
type: ModuleType;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
priority: number;
|
||||
refresh_interval_sec: number | null;
|
||||
cron_expr: string | null;
|
||||
default_community_id: string | null;
|
||||
doh_profile_id: string | null;
|
||||
};
|
||||
export type ModulesResponse = Page<ModuleRow>;
|
||||
|
||||
export type ModuleCreate = {
|
||||
type: ModuleType;
|
||||
name: string;
|
||||
enabled?: boolean;
|
||||
priority?: number;
|
||||
doh_profile_id?: string | null;
|
||||
refresh_interval_sec?: number | null;
|
||||
cron_expr?: string | null;
|
||||
default_community_id?: string | null;
|
||||
};
|
||||
export type ModulePatch = Partial<Omit<ModuleCreate, 'type'>>;
|
||||
|
||||
// ---- AS Entries ----
|
||||
export type AsEntry = {
|
||||
id: string;
|
||||
asn: number | null;
|
||||
prefix: string | null;
|
||||
community_id: string | null;
|
||||
};
|
||||
export type AsEntryCreate = {
|
||||
asn?: number;
|
||||
prefix?: string;
|
||||
community_id?: string | null;
|
||||
};
|
||||
export type AsEntriesResponse = Page<AsEntry>;
|
||||
|
||||
// ---- CDN Sources ----
|
||||
export type CdnSource = {
|
||||
id: string;
|
||||
url: string;
|
||||
source_kind: string;
|
||||
community_id: string | null;
|
||||
refresh_interval_sec: number | null;
|
||||
};
|
||||
export type CdnSourceCreate = {
|
||||
url: string;
|
||||
source_kind: string;
|
||||
community_id?: string | null;
|
||||
};
|
||||
export type CdnSourcePatch = Partial<CdnSourceCreate> & { refresh_interval_sec?: number | null };
|
||||
export type CdnSourcesResponse = Page<CdnSource>;
|
||||
|
||||
// ---- Domain Entries ----
|
||||
export type DomainEntry = {
|
||||
id: string;
|
||||
fqdn: string;
|
||||
community_id: string | null;
|
||||
};
|
||||
export type DomainEntryCreate = {
|
||||
fqdn: string;
|
||||
community_id?: string | null;
|
||||
};
|
||||
export type DomainEntriesResponse = Page<DomainEntry>;
|
||||
|
||||
// ---- IP Range Entries ----
|
||||
export type IpRangeEntry = {
|
||||
id: string;
|
||||
prefix: string;
|
||||
community_id: string;
|
||||
};
|
||||
export type IpRangeEntryCreate = {
|
||||
prefix: string;
|
||||
community_id: string;
|
||||
};
|
||||
export type IpRangeEntriesResponse = Page<IpRangeEntry>;
|
||||
|
||||
// ---- DoH Profiles ----
|
||||
export type DohProfile = {
|
||||
id: string;
|
||||
url: string;
|
||||
timeout_ms: number | null;
|
||||
vault_secret_ref: string | null;
|
||||
};
|
||||
export type DohProfileCreate = {
|
||||
url: string;
|
||||
timeout_ms?: number | null;
|
||||
vault_secret_ref?: string | null;
|
||||
};
|
||||
export type DohProfilePatch = Partial<DohProfileCreate>;
|
||||
export type DohProfilesResponse = Page<DohProfile>;
|
||||
|
||||
// ---- Communities ----
|
||||
export type BgpCommunity = {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: string | null;
|
||||
};
|
||||
export type BgpCommunityCreate = {
|
||||
name: string;
|
||||
kind?: string;
|
||||
};
|
||||
export type BgpCommunityPatch = Partial<BgpCommunityCreate>;
|
||||
export type CommunitiesResponse = Page<BgpCommunity>;
|
||||
|
||||
// ---- Peers ----
|
||||
export type PeerRow = {
|
||||
id: string;
|
||||
name?: string;
|
||||
neighbor: string;
|
||||
remote_asn?: number;
|
||||
session_state: string;
|
||||
bgp_speaker_id: string | null;
|
||||
};
|
||||
export type PeersResponse = Page<PeerRow>;
|
||||
export type BgpPeerCreate = {
|
||||
neighbor: string;
|
||||
remote_asn: number;
|
||||
bgp_speaker_id?: string | null;
|
||||
};
|
||||
export type BgpPeerPatch = Partial<BgpPeerCreate>;
|
||||
|
||||
// ---- Speakers ----
|
||||
export type SpeakerRow = {
|
||||
id: string;
|
||||
role: string;
|
||||
endpoint: string;
|
||||
last_applied_revision_id: string | null;
|
||||
};
|
||||
export type SpeakersResponse = Page<SpeakerRow>;
|
||||
export type BgpSpeakerCreate = {
|
||||
endpoint: string;
|
||||
role?: string;
|
||||
};
|
||||
export type BgpSpeakerPatch = Partial<BgpSpeakerCreate>;
|
||||
|
||||
// ---- Revisions ----
|
||||
export type RevisionRow = {
|
||||
id: string;
|
||||
content_hash: string;
|
||||
@@ -24,40 +155,27 @@ export type RevisionRow = {
|
||||
materialized_prefix_count: number;
|
||||
module_id: string | null;
|
||||
};
|
||||
export type RevisionsResponse = Page<RevisionRow>;
|
||||
|
||||
export type RevisionsResponse = {
|
||||
items: RevisionRow[];
|
||||
next_cursor: string | null;
|
||||
has_more: boolean;
|
||||
export type RevisionPrefix = {
|
||||
prefix: string;
|
||||
community_id?: string | null;
|
||||
};
|
||||
export type RevisionPrefixesResponse = Page<RevisionPrefix>;
|
||||
|
||||
export type PeerRow = {
|
||||
export type RevisionPreview = {
|
||||
id: string;
|
||||
name: string;
|
||||
neighbor: string;
|
||||
session_state: string;
|
||||
bgp_speaker_id: string | null;
|
||||
prefixes?: RevisionPrefix[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type PeersResponse = {
|
||||
items: PeerRow[];
|
||||
next_cursor: string | null;
|
||||
has_more: boolean;
|
||||
};
|
||||
|
||||
export type SpeakerRow = {
|
||||
id: string;
|
||||
role: string;
|
||||
endpoint: string;
|
||||
last_applied_revision_id: string | null;
|
||||
};
|
||||
|
||||
export type SpeakersResponse = {
|
||||
items: SpeakerRow[];
|
||||
next_cursor: string | null;
|
||||
has_more: boolean;
|
||||
export type RevisionDiff = {
|
||||
added: RevisionPrefix[];
|
||||
removed: RevisionPrefix[];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
// ---- Jobs ----
|
||||
export type JobRow = {
|
||||
job_id: string;
|
||||
kind: string;
|
||||
@@ -69,9 +187,7 @@ export type JobRow = {
|
||||
error?: string | null;
|
||||
meta?: Record<string, unknown>;
|
||||
};
|
||||
export type JobsResponse = Page<JobRow>;
|
||||
|
||||
export type JobsResponse = {
|
||||
items: JobRow[];
|
||||
next_cursor: string | null;
|
||||
has_more: boolean;
|
||||
};
|
||||
// ---- Settings ----
|
||||
export type AppSettings = Record<string, unknown>;
|
||||
|
||||
Reference in New Issue
Block a user