Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0a912a693 | ||
|
|
9740a34fdc |
@@ -2379,6 +2379,21 @@ paths:
|
|||||||
$ref: "#/components/responses/NotFound"
|
$ref: "#/components/responses/NotFound"
|
||||||
default:
|
default:
|
||||||
$ref: "#/components/responses/DefaultProblem"
|
$ref: "#/components/responses/DefaultProblem"
|
||||||
|
delete:
|
||||||
|
tags: [Speakers]
|
||||||
|
summary: Удалить спикер
|
||||||
|
description: >
|
||||||
|
Удаляет BGP-спикер. Пиры с `bgp_speaker_id` этого спикера остаются, привязка сбрасывается (ON DELETE SET NULL).
|
||||||
|
operationId: deleteSpeaker
|
||||||
|
parameters:
|
||||||
|
- $ref: "#/components/parameters/IdempotencyKey"
|
||||||
|
responses:
|
||||||
|
"204":
|
||||||
|
description: Удалено.
|
||||||
|
"404":
|
||||||
|
$ref: "#/components/responses/NotFound"
|
||||||
|
default:
|
||||||
|
$ref: "#/components/responses/DefaultProblem"
|
||||||
|
|
||||||
/v1/revisions:
|
/v1/revisions:
|
||||||
get:
|
get:
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ func (s *Server) registerCRUDRoutes(m *http.ServeMux) {
|
|||||||
m.HandleFunc("POST /speakers", s.handlePostSpeaker)
|
m.HandleFunc("POST /speakers", s.handlePostSpeaker)
|
||||||
m.HandleFunc("GET /speakers/{speaker_id}", s.handleGetSpeakerByID)
|
m.HandleFunc("GET /speakers/{speaker_id}", s.handleGetSpeakerByID)
|
||||||
m.HandleFunc("PATCH /speakers/{speaker_id}", s.handlePatchSpeaker)
|
m.HandleFunc("PATCH /speakers/{speaker_id}", s.handlePatchSpeaker)
|
||||||
|
m.HandleFunc("DELETE /speakers/{speaker_id}", s.handleDeleteSpeaker)
|
||||||
|
|
||||||
m.HandleFunc("GET /revisions/{revision_id}/prefixes", s.handleRevisionPrefixes)
|
m.HandleFunc("GET /revisions/{revision_id}/prefixes", s.handleRevisionPrefixes)
|
||||||
|
|
||||||
@@ -1021,6 +1022,18 @@ func (s *Server) handlePatchSpeaker(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusOK, speakerJSONFromStore(s.store, x))
|
writeJSON(w, http.StatusOK, speakerJSONFromStore(s.store, x))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDeleteSpeaker(w http.ResponseWriter, r *http.Request) {
|
||||||
|
a, ok := authFromContext(r.Context())
|
||||||
|
if !ok || !s.requireAtLeast(w, a, "editor") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := s.store.DeleteSpeaker(a.TenantID, r.PathValue("speaker_id")); err != nil {
|
||||||
|
writeStoreErr(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleRevisionPrefixes(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleRevisionPrefixes(w http.ResponseWriter, r *http.Request) {
|
||||||
a, ok := authFromContext(r.Context())
|
a, ok := authFromContext(r.Context())
|
||||||
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
if !ok || !s.requireAtLeast(w, a, "viewer") {
|
||||||
|
|||||||
@@ -41,6 +41,27 @@ func TestPostSpeaker_defaultsFromEndpointIP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeleteSpeaker(t *testing.T) {
|
||||||
|
srv, err := New(Options{InsecureDev: true, SeedDemo: true, BundleSeedHex: testBundleSeed})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer srv.Close()
|
||||||
|
tenant, _, _, _, demoSpk := srv.Store().DemoIDs()
|
||||||
|
mustSetTestAPIKeys(t, srv, "edkey|"+tenant+"|editor")
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/v1/speakers/"+demoSpk, nil)
|
||||||
|
req.Header.Set("Authorization", "Bearer edkey")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
srv.Handler().ServeHTTP(rec, req)
|
||||||
|
if rec.Code != http.StatusNoContent {
|
||||||
|
t.Fatalf("status %d body %s", rec.Code, rec.Body.String())
|
||||||
|
}
|
||||||
|
if _, err := srv.Store().GetSpeaker(tenant, demoSpk); err == nil {
|
||||||
|
t.Fatal("speaker should be deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetBundleSigningPublicKey(t *testing.T) {
|
func TestGetBundleSigningPublicKey(t *testing.T) {
|
||||||
srv, err := New(Options{InsecureDev: true, SeedDemo: true, BundleSeedHex: testBundleSeed})
|
srv, err := New(Options{InsecureDev: true, SeedDemo: true, BundleSeedHex: testBundleSeed})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -219,8 +219,8 @@ func (w *Worker) runPeerReconcile(j *Job) {
|
|||||||
} else {
|
} else {
|
||||||
j.mergeMeta(map[string]any{"log_build_error": err.Error()})
|
j.mergeMeta(map[string]any{"log_build_error": err.Error()})
|
||||||
}
|
}
|
||||||
j.Succeed()
|
|
||||||
w.enqueueDeployAllSpeakers(j, j.TenantID, revID)
|
w.enqueueDeployAllSpeakers(j, j.TenantID, revID)
|
||||||
|
j.Succeed()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Worker) peerTriggerModuleID(tenantID string, latest []*store.Revision) (string, error) {
|
func (w *Worker) peerTriggerModuleID(tenantID string, latest []*store.Revision) (string, error) {
|
||||||
@@ -346,8 +346,8 @@ func (w *Worker) finishModuleRefreshSuccess(j *Job, triggerModuleID string) {
|
|||||||
} else {
|
} else {
|
||||||
j.mergeMeta(map[string]any{"log_build_error": err.Error()})
|
j.mergeMeta(map[string]any{"log_build_error": err.Error()})
|
||||||
}
|
}
|
||||||
j.Succeed()
|
|
||||||
w.enqueueDeployAllSpeakers(j, j.TenantID, rev)
|
w.enqueueDeployAllSpeakers(j, j.TenantID, rev)
|
||||||
|
j.Succeed()
|
||||||
}
|
}
|
||||||
|
|
||||||
// enqueueDeployAllSpeakers queues the same work as POST /v1/apply (all speakers, no speaker_id).
|
// enqueueDeployAllSpeakers queues the same work as POST /v1/apply (all speakers, no speaker_id).
|
||||||
|
|||||||
@@ -646,6 +646,18 @@ func (p *Postgres) UpdateSpeaker(tenantID, id string, patch *store.SpeakerPatch)
|
|||||||
return p.GetSpeaker(tenantID, id)
|
return p.GetSpeaker(tenantID, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Postgres) DeleteSpeaker(tenantID, id string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
tag, err := p.pool.Exec(ctx, `DELETE FROM bgp_speaker WHERE id=$1 AND tenant_id=$2`, id, tenantID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if tag.RowsAffected() == 0 {
|
||||||
|
return store.ErrNotFound
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Postgres) GetRevision(tenantID, revisionID string) (*store.Revision, error) {
|
func (p *Postgres) GetRevision(tenantID, revisionID string) (*store.Revision, error) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
var r store.Revision
|
var r store.Revision
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ type Backend interface {
|
|||||||
GetSpeakerAnyTenant(speakerID string) (*Speaker, error)
|
GetSpeakerAnyTenant(speakerID string) (*Speaker, error)
|
||||||
CreateSpeaker(tenantID string, in *Speaker) (*Speaker, error)
|
CreateSpeaker(tenantID string, in *Speaker) (*Speaker, error)
|
||||||
UpdateSpeaker(tenantID, id string, patch *SpeakerPatch) (*Speaker, error)
|
UpdateSpeaker(tenantID, id string, patch *SpeakerPatch) (*Speaker, error)
|
||||||
|
DeleteSpeaker(tenantID, id string) error
|
||||||
|
|
||||||
GetRevision(tenantID, revisionID string) (*Revision, error)
|
GetRevision(tenantID, revisionID string) (*Revision, error)
|
||||||
ListRevisions(tenantID, moduleID string, cursor string, limit int) (items []*Revision, nextCursor string, hasMore bool)
|
ListRevisions(tenantID, moduleID string, cursor string, limit int) (items []*Revision, nextCursor string, hasMore bool)
|
||||||
|
|||||||
@@ -779,6 +779,18 @@ func (m *Memory) UpdateSpeaker(tenantID, id string, patch *SpeakerPatch) (*Speak
|
|||||||
return sp, nil
|
return sp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Memory) DeleteSpeaker(tenantID, id string) error {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
sp, ok := m.speakers[id]
|
||||||
|
if !ok || sp.TenantID != tenantID {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
delete(m.speakers, id)
|
||||||
|
delete(m.publishedRevision, id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Memory) ListRevisionPrefixes(tenantID, revisionID string, cursor string, limit int) ([]PrefixRow, string, bool) {
|
func (m *Memory) ListRevisionPrefixes(tenantID, revisionID string, cursor string, limit int) ([]PrefixRow, string, bool) {
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
limit = 50
|
limit = 50
|
||||||
|
|||||||
@@ -22,11 +22,13 @@
|
|||||||
import FormField from '$lib/ui/patterns/form/form-field.svelte';
|
import FormField from '$lib/ui/patterns/form/form-field.svelte';
|
||||||
import AppInput from '$lib/ui/patterns/form/app-input.svelte';
|
import AppInput from '$lib/ui/patterns/form/app-input.svelte';
|
||||||
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
|
import AppDataTable from '$lib/ui/patterns/data-table/app-data-table.svelte';
|
||||||
|
import { confirm } from '$lib/ui/patterns/confirm/confirm-state.svelte.js';
|
||||||
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
|
import { notify, notifyApiError } from '$lib/ui/app/toast.js';
|
||||||
import Plus from '@lucide/svelte/icons/plus';
|
import Plus from '@lucide/svelte/icons/plus';
|
||||||
import Pencil from '@lucide/svelte/icons/pencil';
|
import Pencil from '@lucide/svelte/icons/pencil';
|
||||||
import Play from '@lucide/svelte/icons/play';
|
import Play from '@lucide/svelte/icons/play';
|
||||||
import Copy from '@lucide/svelte/icons/copy';
|
import Copy from '@lucide/svelte/icons/copy';
|
||||||
|
import Trash2 from '@lucide/svelte/icons/trash-2';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
items: SpeakerRow[];
|
items: SpeakerRow[];
|
||||||
@@ -180,6 +182,21 @@
|
|||||||
dialogOpen = true;
|
dialogOpen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requestDelete(s: SpeakerRow) {
|
||||||
|
const label = s.agent_domain ?? s.endpoint ?? s.id;
|
||||||
|
void confirm({
|
||||||
|
title: 'Удалить спикера?',
|
||||||
|
description: label,
|
||||||
|
confirmLabel: 'Удалить',
|
||||||
|
destructive: true,
|
||||||
|
onConfirm: async () => {
|
||||||
|
await apiMutate(`/v1/speakers/${s.id}`, 'DELETE', undefined, { idempotent: false });
|
||||||
|
notify.success('Спикер удалён');
|
||||||
|
await onRefresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function openApply(s: SpeakerRow) {
|
function openApply(s: SpeakerRow) {
|
||||||
applyTarget = s;
|
applyTarget = s;
|
||||||
applyRevisionId = s.published_revision_id ?? '';
|
applyRevisionId = s.published_revision_id ?? '';
|
||||||
@@ -341,6 +358,15 @@ CF_DNS_API_TOKEN=<cloudflare token>
|
|||||||
<Button variant="ghost" size="icon-sm" onclick={() => openEdit(s)}>
|
<Button variant="ghost" size="icon-sm" onclick={() => openEdit(s)}>
|
||||||
<Pencil class="size-3.5" />
|
<Pencil class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon-sm"
|
||||||
|
class="text-destructive"
|
||||||
|
title="Удалить спикера"
|
||||||
|
onclick={() => requestDelete(s)}
|
||||||
|
>
|
||||||
|
<Trash2 class="size-3.5" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
Reference in New Issue
Block a user