Added rules frontend
This commit is contained in:
@@ -175,22 +175,26 @@ func (c *CachedSiteRepository) CreateForwardRule(siteID string, fr *models.Forwa
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) UpdateForwardRule(fr *models.ForwardRule) error {
|
||||
if err := c.inner.UpdateForwardRule(fr); err != nil {
|
||||
func (c *CachedSiteRepository) UpdateForwardRule(siteID string, fr *models.ForwardRule) error {
|
||||
if err := c.inner.UpdateForwardRule(siteID, fr); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.forwardRules[fr.ID] = fr
|
||||
delete(c.sites, siteID)
|
||||
c.siteListValid = false
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) DeleteForwardRule(id string) error {
|
||||
if err := c.inner.DeleteForwardRule(id); err != nil {
|
||||
func (c *CachedSiteRepository) DeleteForwardRule(siteID string, id string) error {
|
||||
if err := c.inner.DeleteForwardRule(siteID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
delete(c.forwardRules, id)
|
||||
delete(c.sites, siteID)
|
||||
c.siteListValid = false
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
@@ -228,22 +232,26 @@ func (c *CachedSiteRepository) CreateCustomHeaders(siteID string, ch *models.Cus
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) UpdateCustomHeaders(ch *models.CustomHeaders) error {
|
||||
if err := c.inner.UpdateCustomHeaders(ch); err != nil {
|
||||
func (c *CachedSiteRepository) UpdateCustomHeaders(siteID string, ch *models.CustomHeaders) error {
|
||||
if err := c.inner.UpdateCustomHeaders(siteID, ch); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.customHeaders[ch.ID] = ch
|
||||
delete(c.sites, siteID)
|
||||
c.siteListValid = false
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) DeleteCustomHeaders(id string) error {
|
||||
if err := c.inner.DeleteCustomHeaders(id); err != nil {
|
||||
func (c *CachedSiteRepository) DeleteCustomHeaders(siteID string, id string) error {
|
||||
if err := c.inner.DeleteCustomHeaders(siteID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
delete(c.customHeaders, id)
|
||||
delete(c.sites, siteID)
|
||||
c.siteListValid = false
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
@@ -269,33 +277,39 @@ func (c *CachedSiteRepository) GetHeader(id string) (*models.Header, error) {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) CreateHeader(customHeaderID string, h *models.Header) error {
|
||||
if err := c.inner.CreateHeader(customHeaderID, h); err != nil {
|
||||
func (c *CachedSiteRepository) CreateHeader(siteID string, customHeaderID string, h *models.Header) error {
|
||||
if err := c.inner.CreateHeader(siteID, customHeaderID, h); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.headers[h.ID] = h
|
||||
delete(c.customHeaders, customHeaderID)
|
||||
delete(c.sites, siteID)
|
||||
c.siteListValid = false
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) UpdateHeader(h *models.Header) error {
|
||||
if err := c.inner.UpdateHeader(h); err != nil {
|
||||
func (c *CachedSiteRepository) UpdateHeader(siteID string, h *models.Header) error {
|
||||
if err := c.inner.UpdateHeader(siteID, h); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.headers[h.ID] = h
|
||||
delete(c.sites, siteID)
|
||||
c.siteListValid = false
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CachedSiteRepository) DeleteHeader(id string) error {
|
||||
if err := c.inner.DeleteHeader(id); err != nil {
|
||||
func (c *CachedSiteRepository) DeleteHeader(siteID string, id string) error {
|
||||
if err := c.inner.DeleteHeader(siteID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
delete(c.headers, id)
|
||||
delete(c.sites, siteID)
|
||||
c.siteListValid = false
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ func (h *SiteHandler) GetSiteForwardRules(c fiber.Ctx) error {
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /forward-rules/{id} [get]
|
||||
func (h *SiteHandler) GetForwardRule(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
id := c.Params("ruleId")
|
||||
|
||||
rule, err := h.Repo.GetForwardRule(id)
|
||||
if err != nil {
|
||||
@@ -169,7 +169,8 @@ func (h *SiteHandler) PostForwardRule(c fiber.Ctx) error {
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /forward-rules/{id} [put]
|
||||
func (h *SiteHandler) PutForwardRule(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
siteID := c.Params("id")
|
||||
id := c.Params("ruleId")
|
||||
|
||||
if _, err := h.Repo.GetForwardRule(id); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -190,7 +191,7 @@ func (h *SiteHandler) PutForwardRule(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
rule.ID = id
|
||||
if err := h.Repo.UpdateForwardRule(&rule); err != nil {
|
||||
if err := h.Repo.UpdateForwardRule(siteID, &rule); err != nil {
|
||||
log.Println("Error updating forward rule: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating forward rule"})
|
||||
}
|
||||
@@ -216,7 +217,8 @@ func (h *SiteHandler) PutForwardRule(c fiber.Ctx) error {
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /forward-rules/{id} [delete]
|
||||
func (h *SiteHandler) DeleteForwardRule(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
siteID := c.Params("id")
|
||||
id := c.Params("ruleId")
|
||||
|
||||
if _, err := h.Repo.GetForwardRule(id); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -226,7 +228,7 @@ func (h *SiteHandler) DeleteForwardRule(c fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting forward rule"})
|
||||
}
|
||||
|
||||
if err := h.Repo.DeleteForwardRule(id); err != nil {
|
||||
if err := h.Repo.DeleteForwardRule(siteID, id); err != nil {
|
||||
log.Println("Error deleting forward rule: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting forward rule"})
|
||||
}
|
||||
@@ -275,7 +277,7 @@ func (h *SiteHandler) GetSiteCustomHeaders(c fiber.Ctx) error {
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /custom-headers/{id} [get]
|
||||
func (h *SiteHandler) GetCustomHeaders(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
id := c.Params("customHeaderId")
|
||||
|
||||
customHeaders, err := h.Repo.GetCustomHeaders(id)
|
||||
if err != nil {
|
||||
@@ -347,7 +349,8 @@ func (h *SiteHandler) PostCustomHeaders(c fiber.Ctx) error {
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /custom-headers/{id} [put]
|
||||
func (h *SiteHandler) PutCustomHeaders(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
siteID := c.Params("id")
|
||||
id := c.Params("customHeaderId")
|
||||
|
||||
if _, err := h.Repo.GetCustomHeaders(id); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -368,7 +371,7 @@ func (h *SiteHandler) PutCustomHeaders(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
customHeaders.ID = id
|
||||
if err := h.Repo.UpdateCustomHeaders(&customHeaders); err != nil {
|
||||
if err := h.Repo.UpdateCustomHeaders(siteID, &customHeaders); err != nil {
|
||||
log.Println("Error updating custom headers: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating custom headers"})
|
||||
}
|
||||
@@ -394,7 +397,8 @@ func (h *SiteHandler) PutCustomHeaders(c fiber.Ctx) error {
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /custom-headers/{id} [delete]
|
||||
func (h *SiteHandler) DeleteCustomHeaders(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
siteID := c.Params("id")
|
||||
id := c.Params("customHeaderId")
|
||||
|
||||
if _, err := h.Repo.GetCustomHeaders(id); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -404,7 +408,7 @@ func (h *SiteHandler) DeleteCustomHeaders(c fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting custom headers"})
|
||||
}
|
||||
|
||||
if err := h.Repo.DeleteCustomHeaders(id); err != nil {
|
||||
if err := h.Repo.DeleteCustomHeaders(siteID, id); err != nil {
|
||||
log.Println("Error deleting custom headers: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting custom headers"})
|
||||
}
|
||||
@@ -424,7 +428,7 @@ func (h *SiteHandler) DeleteCustomHeaders(c fiber.Ctx) error {
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /custom-headers/{id}/headers [get]
|
||||
func (h *SiteHandler) GetCustomHeaderHeaders(c fiber.Ctx) error {
|
||||
customHeaderID := c.Params("id")
|
||||
customHeaderID := c.Params("customHeaderId")
|
||||
|
||||
customHeaders, err := h.Repo.GetCustomHeaders(customHeaderID)
|
||||
if err != nil {
|
||||
@@ -453,7 +457,7 @@ func (h *SiteHandler) GetCustomHeaderHeaders(c fiber.Ctx) error {
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /headers/{id} [get]
|
||||
func (h *SiteHandler) GetHeader(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
id := c.Params("headerId")
|
||||
|
||||
header, err := h.Repo.GetHeader(id)
|
||||
if err != nil {
|
||||
@@ -482,7 +486,8 @@ func (h *SiteHandler) GetHeader(c fiber.Ctx) error {
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /custom-headers/{id}/headers [post]
|
||||
func (h *SiteHandler) PostHeader(c fiber.Ctx) error {
|
||||
customHeaderID := c.Params("id")
|
||||
siteID := c.Params("id")
|
||||
customHeaderID := c.Params("customHeaderId")
|
||||
|
||||
if _, err := h.Repo.GetCustomHeaders(customHeaderID); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -502,7 +507,7 @@ func (h *SiteHandler) PostHeader(c fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(&models.APIError{Message: "Invalid request body: " + err.Error()})
|
||||
}
|
||||
|
||||
if err := h.Repo.CreateHeader(customHeaderID, &header); err != nil {
|
||||
if err := h.Repo.CreateHeader(siteID, customHeaderID, &header); err != nil {
|
||||
log.Println("Error creating header: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating header"})
|
||||
}
|
||||
@@ -525,7 +530,8 @@ func (h *SiteHandler) PostHeader(c fiber.Ctx) error {
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /headers/{id} [put]
|
||||
func (h *SiteHandler) PutHeader(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
siteID := c.Params("id")
|
||||
id := c.Params("headerId")
|
||||
|
||||
if _, err := h.Repo.GetHeader(id); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -546,7 +552,7 @@ func (h *SiteHandler) PutHeader(c fiber.Ctx) error {
|
||||
}
|
||||
|
||||
header.ID = id
|
||||
if err := h.Repo.UpdateHeader(&header); err != nil {
|
||||
if err := h.Repo.UpdateHeader(siteID, &header); err != nil {
|
||||
log.Println("Error updating header: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating header"})
|
||||
}
|
||||
@@ -572,7 +578,8 @@ func (h *SiteHandler) PutHeader(c fiber.Ctx) error {
|
||||
// @Failure 500 {object} models.APIError "Internal server error"
|
||||
// @Router /headers/{id} [delete]
|
||||
func (h *SiteHandler) DeleteHeader(c fiber.Ctx) error {
|
||||
id := c.Params("id")
|
||||
siteID := c.Params("id")
|
||||
id := c.Params("headerId")
|
||||
|
||||
if _, err := h.Repo.GetHeader(id); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -582,7 +589,7 @@ func (h *SiteHandler) DeleteHeader(c fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting header"})
|
||||
}
|
||||
|
||||
if err := h.Repo.DeleteHeader(id); err != nil {
|
||||
if err := h.Repo.DeleteHeader(siteID, id); err != nil {
|
||||
log.Println("Error deleting header: ", err)
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting header"})
|
||||
}
|
||||
|
||||
@@ -12,14 +12,14 @@ type SiteRepository interface {
|
||||
DeleteSite(id string) error
|
||||
GetForwardRule(id string) (*models.ForwardRule, error)
|
||||
CreateForwardRule(siteID string, fr *models.ForwardRule) error
|
||||
UpdateForwardRule(fr *models.ForwardRule) error
|
||||
DeleteForwardRule(id string) error
|
||||
UpdateForwardRule(siteID string, fr *models.ForwardRule) error
|
||||
DeleteForwardRule(siteID string, id string) error
|
||||
GetCustomHeaders(id string) (*models.CustomHeaders, error)
|
||||
CreateCustomHeaders(siteID string, ch *models.CustomHeaders) error
|
||||
UpdateCustomHeaders(ch *models.CustomHeaders) error
|
||||
DeleteCustomHeaders(id string) error
|
||||
UpdateCustomHeaders(siteID string, ch *models.CustomHeaders) error
|
||||
DeleteCustomHeaders(siteID string, id string) error
|
||||
GetHeader(id string) (*models.Header, error)
|
||||
CreateHeader(customHeaderID string, h *models.Header) error
|
||||
UpdateHeader(h *models.Header) error
|
||||
DeleteHeader(id string) error
|
||||
CreateHeader(siteID string, customHeaderID string, h *models.Header) error
|
||||
UpdateHeader(siteID string, h *models.Header) error
|
||||
DeleteHeader(siteID string, id string) error
|
||||
}
|
||||
|
||||
@@ -39,23 +39,23 @@ func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, d
|
||||
// Forward rules
|
||||
api.Get("/sites/:id/forward-rules", siteHandler.GetSiteForwardRules)
|
||||
api.Post("/sites/:id/forward-rules", siteHandler.PostForwardRule)
|
||||
api.Get("/forward-rules/:id", siteHandler.GetForwardRule)
|
||||
api.Put("/forward-rules/:id", siteHandler.PutForwardRule)
|
||||
api.Delete("/forward-rules/:id", siteHandler.DeleteForwardRule)
|
||||
api.Get("/sites/:id/forward-rules/:ruleId", siteHandler.GetForwardRule)
|
||||
api.Put("/sites/:id/forward-rules/:ruleId", siteHandler.PutForwardRule)
|
||||
api.Delete("/sites/:id/forward-rules/:ruleId", siteHandler.DeleteForwardRule)
|
||||
|
||||
// Custom headers (header rules)
|
||||
api.Get("/sites/:id/custom-headers", siteHandler.GetSiteCustomHeaders)
|
||||
api.Post("/sites/:id/custom-headers", siteHandler.PostCustomHeaders)
|
||||
api.Get("/custom-headers/:id", siteHandler.GetCustomHeaders)
|
||||
api.Put("/custom-headers/:id", siteHandler.PutCustomHeaders)
|
||||
api.Delete("/custom-headers/:id", siteHandler.DeleteCustomHeaders)
|
||||
api.Get("/sites/:id/custom-headers/:customHeaderId", siteHandler.GetCustomHeaders)
|
||||
api.Put("/sites/:id/custom-headers/:customHeaderId", siteHandler.PutCustomHeaders)
|
||||
api.Delete("/sites/:id/custom-headers/:customHeaderId", siteHandler.DeleteCustomHeaders)
|
||||
|
||||
// Headers
|
||||
api.Get("/custom-headers/:id/headers", siteHandler.GetCustomHeaderHeaders)
|
||||
api.Post("/custom-headers/:id/headers", siteHandler.PostHeader)
|
||||
api.Get("/headers/:id", siteHandler.GetHeader)
|
||||
api.Put("/headers/:id", siteHandler.PutHeader)
|
||||
api.Delete("/headers/:id", siteHandler.DeleteHeader)
|
||||
api.Get("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.GetCustomHeaderHeaders)
|
||||
api.Post("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.PostHeader)
|
||||
api.Get("/sites/:id/headers/:headerId", siteHandler.GetHeader)
|
||||
api.Put("/sites/:id/headers/:headerId", siteHandler.PutHeader)
|
||||
api.Delete("/sites/:id/headers/:headerId", siteHandler.DeleteHeader)
|
||||
|
||||
// Deployments
|
||||
api.Get("/deployments/:id", deploymentsHandler.GetDeployment)
|
||||
|
||||
@@ -233,7 +233,7 @@ func (r *SQLiteSiteRepository) CreateForwardRule(siteID string, fr *models.Forwa
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) UpdateForwardRule(fr *models.ForwardRule) error {
|
||||
func (r *SQLiteSiteRepository) UpdateForwardRule(siteID string, fr *models.ForwardRule) error {
|
||||
_, err := r.db.Exec(`
|
||||
UPDATE forward_rules SET source=?, destination=?, status_code=?, regex=? WHERE id=?`,
|
||||
fr.Source, fr.Destination, fr.StatusCode, fr.Regex, fr.ID,
|
||||
@@ -244,7 +244,7 @@ func (r *SQLiteSiteRepository) UpdateForwardRule(fr *models.ForwardRule) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) DeleteForwardRule(id string) error {
|
||||
func (r *SQLiteSiteRepository) DeleteForwardRule(_ string, id string) error {
|
||||
_, err := r.db.Exec(`DELETE FROM forward_rules WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete forward rule: %w", err)
|
||||
@@ -284,7 +284,7 @@ func (r *SQLiteSiteRepository) CreateCustomHeaders(siteID string, ch *models.Cus
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) UpdateCustomHeaders(ch *models.CustomHeaders) error {
|
||||
func (r *SQLiteSiteRepository) UpdateCustomHeaders(siteID string, ch *models.CustomHeaders) error {
|
||||
tx, err := r.db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("update custom headers begin tx: %w", err)
|
||||
@@ -305,7 +305,7 @@ func (r *SQLiteSiteRepository) UpdateCustomHeaders(ch *models.CustomHeaders) err
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) DeleteCustomHeaders(id string) error {
|
||||
func (r *SQLiteSiteRepository) DeleteCustomHeaders(_ string, id string) error {
|
||||
_, err := r.db.Exec(`DELETE FROM custom_headers WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete custom headers: %w", err)
|
||||
@@ -325,7 +325,7 @@ func (r *SQLiteSiteRepository) GetHeader(id string) (*models.Header, error) {
|
||||
return &h, nil
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) CreateHeader(customHeaderID string, h *models.Header) error {
|
||||
func (r *SQLiteSiteRepository) CreateHeader(_ string, customHeaderID string, h *models.Header) error {
|
||||
h.ID = uuid.NewString()
|
||||
_, err := r.db.Exec(
|
||||
`INSERT INTO headers (id, custom_header_id, key, value) VALUES (?, ?, ?, ?)`,
|
||||
@@ -337,7 +337,7 @@ func (r *SQLiteSiteRepository) CreateHeader(customHeaderID string, h *models.Hea
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) UpdateHeader(h *models.Header) error {
|
||||
func (r *SQLiteSiteRepository) UpdateHeader(_ string, h *models.Header) error {
|
||||
_, err := r.db.Exec(`UPDATE headers SET key=?, value=? WHERE id=?`, h.Key, h.Value, h.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update header: %w", err)
|
||||
@@ -345,7 +345,7 @@ func (r *SQLiteSiteRepository) UpdateHeader(h *models.Header) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SQLiteSiteRepository) DeleteHeader(id string) error {
|
||||
func (r *SQLiteSiteRepository) DeleteHeader(_ string, id string) error {
|
||||
_, err := r.db.Exec(`DELETE FROM headers WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete header: %w", err)
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { makeApiUrl } from '.';
|
||||
import type {
|
||||
CreateCustomHeadersRequest,
|
||||
CreateHeaderRequest,
|
||||
CustomHeaders,
|
||||
Header,
|
||||
} from './types/site';
|
||||
|
||||
export const getCustomHeaders = async (siteId: string): Promise<CustomHeaders[]> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/custom-headers`), {
|
||||
method: 'GET',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch custom headers');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const createCustomHeaders = async (
|
||||
siteId: string,
|
||||
data: CreateCustomHeadersRequest
|
||||
): Promise<CustomHeaders> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/custom-headers`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create custom headers');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const updateCustomHeaders = async (
|
||||
siteId: string,
|
||||
groupId: string,
|
||||
data: CreateCustomHeadersRequest
|
||||
): Promise<CustomHeaders> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/custom-headers/${groupId}`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update custom headers');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const deleteCustomHeaders = async (siteId: string, groupId: string): Promise<void> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/custom-headers/${groupId}`), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete custom headers');
|
||||
}
|
||||
};
|
||||
|
||||
export const createHeader = async (
|
||||
siteId: string,
|
||||
groupId: string,
|
||||
data: CreateHeaderRequest
|
||||
): Promise<Header> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/custom-headers/${groupId}/headers`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create header');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const updateHeader = async (
|
||||
siteId: string,
|
||||
headerId: string,
|
||||
data: CreateHeaderRequest
|
||||
): Promise<Header> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/headers/${headerId}`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update header');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const deleteHeader = async (siteId: string, headerId: string): Promise<void> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/headers/${headerId}`), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete header');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { makeApiUrl } from '.';
|
||||
import type { CreateForwardRuleRequest, ForwardRule } from './types/site';
|
||||
|
||||
export const getForwardRules = async (siteId: string): Promise<ForwardRule[]> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/forward-rules`), {
|
||||
method: 'GET',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch forward rules');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const createForwardRule = async (
|
||||
siteId: string,
|
||||
data: CreateForwardRuleRequest
|
||||
): Promise<ForwardRule> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/forward-rules`), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create forward rule');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const updateForwardRule = async (
|
||||
siteId: string,
|
||||
ruleId: string,
|
||||
data: CreateForwardRuleRequest
|
||||
): Promise<ForwardRule> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/forward-rules/${ruleId}`), {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to update forward rule');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export const deleteForwardRule = async (siteId: string, ruleId: string): Promise<void> => {
|
||||
const response = await fetch(makeApiUrl(`/sites/${siteId}/forward-rules/${ruleId}`), {
|
||||
method: 'DELETE',
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete forward rule');
|
||||
}
|
||||
};
|
||||
@@ -64,3 +64,20 @@ export interface CreateSiteResponse {
|
||||
export interface ToggleSiteEnabledResponse {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface CreateForwardRuleRequest {
|
||||
source: string;
|
||||
destination: string;
|
||||
status_code: number;
|
||||
regex: boolean;
|
||||
}
|
||||
|
||||
export interface CreateCustomHeadersRequest {
|
||||
source: string;
|
||||
regex: boolean;
|
||||
}
|
||||
|
||||
export interface CreateHeaderRequest {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
import { Accordion as AccordionPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
|
||||
function Accordion({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return (
|
||||
<AccordionPrimitive.Root
|
||||
data-slot="accordion"
|
||||
className={cn("flex w-full flex-col", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("not-last:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"group/accordion-trigger relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:after:border-ring disabled:pointer-events-none disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4 **:data-[slot=accordion-trigger-icon]:text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon data-slot="accordion-trigger-icon" className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden" />
|
||||
<ChevronUpIcon data-slot="accordion-trigger-icon" className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
)
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"h-(--radix-accordion-content-height) pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</AccordionPrimitive.Content>
|
||||
)
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
@@ -0,0 +1,197 @@
|
||||
import * as React from "react"
|
||||
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
|
||||
size?: "default" | "sm"
|
||||
}) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/alert-dialog-content fixed top-1/2 left-1/2 z-50 grid w-full -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none data-[size=default]:max-w-xs data-[size=sm]:max-w-xs data-[size=default]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn(
|
||||
"grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-4 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogMedia({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-media"
|
||||
className={cn(
|
||||
"mb-2 inline-flex size-10 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-6",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn(
|
||||
"text-base font-medium sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn(
|
||||
"text-sm text-balance text-muted-foreground md:text-pretty *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
|
||||
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||
return (
|
||||
<Button variant={variant} size={size} asChild>
|
||||
<AlertDialogPrimitive.Action
|
||||
data-slot="alert-dialog-action"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
variant = "outline",
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
|
||||
Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
|
||||
return (
|
||||
<Button variant={variant} size={size} asChild>
|
||||
<AlertDialogPrimitive.Cancel
|
||||
data-slot="alert-dialog-cancel"
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogMedia,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { CreateForwardRuleRequest } from '../../../api/types/site';
|
||||
import { createForwardRule } from '../../../api/forwardrules.api';
|
||||
|
||||
export function useCreateForwardRule(siteId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (data: CreateForwardRuleRequest) => createForwardRule(siteId, data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({
|
||||
queryKey: ['forwardRules', siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { deleteForwardRule } from '../../../api/forwardrules.api';
|
||||
|
||||
export function useDeleteForwardRule(siteId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (ruleId: string) => deleteForwardRule(siteId, ruleId),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({
|
||||
queryKey: ['forwardRules', siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getForwardRules } from '../../../api/forwardrules.api';
|
||||
|
||||
export function useForwardRules(siteId: string) {
|
||||
return useQuery({
|
||||
queryKey: ['forwardRules', siteId],
|
||||
queryFn: async () => getForwardRules(siteId),
|
||||
enabled: !!siteId,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { updateForwardRule } from '../../../api/forwardrules.api';
|
||||
import type { CreateForwardRuleRequest } from '../../../api/types/site';
|
||||
|
||||
export function useUpdateForwardRule(siteId: string, ruleId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: async (data: CreateForwardRuleRequest) =>
|
||||
updateForwardRule(siteId, ruleId, data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({
|
||||
queryKey: ['forwardRules', siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { CreateCustomHeadersRequest } from '../../api/types/site';
|
||||
import { createCustomHeaders } from '../../api/customHeaders.api';
|
||||
|
||||
export function useCreateCustomHeaders(siteId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateCustomHeadersRequest) => createCustomHeaders(siteId, data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({
|
||||
queryKey: ['customHeaders', siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { CreateHeaderRequest } from '../../api/types/site';
|
||||
import { createHeader } from '../../api/customHeaders.api';
|
||||
|
||||
export function useCreateHeader(siteId: string, groupId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateHeaderRequest) => createHeader(siteId, groupId, data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({
|
||||
queryKey: ['customHeaders', siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getCustomHeaders } from '../../api/customHeaders.api';
|
||||
import type { CustomHeaders } from '../../api/types/site';
|
||||
|
||||
export function useCustomHeaders(siteId: string) {
|
||||
return useQuery<CustomHeaders[]>({
|
||||
queryKey: ['customHeaders', siteId],
|
||||
queryFn: () => getCustomHeaders(siteId),
|
||||
enabled: !!siteId,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { deleteCustomHeaders } from '../../api/customHeaders.api';
|
||||
|
||||
export function useDeleteCustomHeaders(groupId: string, siteId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () => deleteCustomHeaders(siteId, groupId),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({
|
||||
queryKey: ['customHeaders', siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { deleteHeader } from '../../api/customHeaders.api';
|
||||
|
||||
export function useDeleteHeader(siteId: string, headerId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () => deleteHeader(siteId, headerId),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({
|
||||
queryKey: ['customHeaders', siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { CreateCustomHeadersRequest } from '../../api/types/site';
|
||||
import { updateCustomHeaders } from '../../api/customHeaders.api';
|
||||
|
||||
export function useUpdateCustomHeaders(siteId: string, groupId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateCustomHeadersRequest) =>
|
||||
updateCustomHeaders(siteId, groupId, data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({
|
||||
queryKey: ['customHeaders', siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import type { CreateHeaderRequest } from '../../api/types/site';
|
||||
import { updateHeader } from '../../api/customHeaders.api';
|
||||
|
||||
export function useUpdateHeader(siteId: string, headerId: string) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (data: CreateHeaderRequest) => updateHeader(siteId, headerId, data),
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({
|
||||
queryKey: ['customHeaders', siteId],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from '@/components/ui/accordion';
|
||||
import { Check, Pencil, Plus, Route, ShieldPlus, Trash2, X } from 'lucide-react';
|
||||
import type { Site, ForwardRule, CustomHeaders, Header } from '../../api/types/site';
|
||||
import { useForwardRules } from '@/hooks/api/forwardRules/useForwardRules';
|
||||
import { useCreateForwardRule } from '@/hooks/api/forwardRules/useCreateForwardRule';
|
||||
import { useUpdateForwardRule } from '@/hooks/api/forwardRules/useUpdateForwardRule';
|
||||
import { useDeleteForwardRule } from '@/hooks/api/forwardRules/useDeleteForwardRule';
|
||||
import { useCustomHeaders } from '@/hooks/api/useCustomHeaders';
|
||||
import {
|
||||
Empty,
|
||||
EmptyDescription,
|
||||
EmptyHeader as EmptyHdr,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from '@/components/ui/empty';
|
||||
import { useCreateCustomHeaders } from '../../hooks/api/useCreateCustomHeaders';
|
||||
import { useDeleteCustomHeaders } from '../../hooks/api/useDeleteCustomHeaders';
|
||||
import { useCreateHeader } from '../../hooks/api/useCreateHeader';
|
||||
import { useDeleteHeader } from '../../hooks/api/useDeleteHeader';
|
||||
import { useUpdateHeader } from '../../hooks/api/useUpdateHeader';
|
||||
|
||||
interface ForwardRuleFormData {
|
||||
source: string;
|
||||
destination: string;
|
||||
status_code: number;
|
||||
regex: boolean;
|
||||
}
|
||||
|
||||
const EMPTY_FORWARD_RULE: ForwardRuleFormData = {
|
||||
source: '',
|
||||
destination: '',
|
||||
status_code: 301,
|
||||
regex: false,
|
||||
};
|
||||
|
||||
const STATUS_CODE_OPTIONS = [
|
||||
{ value: 301, label: '301 — Permanent Redirect' },
|
||||
{ value: 302, label: '302 — Temporary Redirect' },
|
||||
{ value: 307, label: '307 — Temporary (preserve method)' },
|
||||
{ value: 308, label: '308 — Permanent (preserve method)' },
|
||||
];
|
||||
|
||||
function ForwardRuleDialog({
|
||||
rule,
|
||||
siteId,
|
||||
trigger,
|
||||
}: {
|
||||
rule?: ForwardRule;
|
||||
siteId: string;
|
||||
trigger: React.ReactNode;
|
||||
}) {
|
||||
const isEdit = !!rule;
|
||||
const [open, setOpen] = useState(false);
|
||||
const [form, setForm] = useState<ForwardRuleFormData>(
|
||||
rule
|
||||
? {
|
||||
source: rule.source,
|
||||
destination: rule.destination,
|
||||
status_code: rule.status_code,
|
||||
regex: rule.regex,
|
||||
}
|
||||
: EMPTY_FORWARD_RULE
|
||||
);
|
||||
|
||||
const createRule = useCreateForwardRule(siteId);
|
||||
const updateRule = useUpdateForwardRule(siteId, rule?.id ?? '');
|
||||
|
||||
const handleSubmit = () => {
|
||||
const mutation = isEdit ? updateRule : createRule;
|
||||
mutation.mutate(form, {
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
if (!isEdit) setForm(EMPTY_FORWARD_RULE);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const isPending = createRule.isPending || updateRule.isPending;
|
||||
const isValid = form.source.trim() !== '' && form.destination.trim() !== '';
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isEdit ? 'Edit' : 'New'} Forward Rule</DialogTitle>
|
||||
<DialogDescription>
|
||||
{isEdit
|
||||
? 'Update the source, destination, and options for this rule.'
|
||||
: 'Redirect requests from one path to another.'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="fr-source">Source path</Label>
|
||||
<Input
|
||||
id="fr-source"
|
||||
placeholder="/old-page"
|
||||
value={form.source}
|
||||
onChange={(e) => setForm((f) => ({ ...f, source: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="fr-dest">Destination</Label>
|
||||
<Input
|
||||
id="fr-dest"
|
||||
placeholder="/new-page or https://example.com"
|
||||
value={form.destination}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, destination: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Status Code</Label>
|
||||
<Select
|
||||
value={String(form.status_code)}
|
||||
onValueChange={(v) =>
|
||||
setForm((f) => ({ ...f, status_code: Number(v) }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{STATUS_CODE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={String(opt.value)}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Label className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={form.regex}
|
||||
onCheckedChange={(v) => setForm((f) => ({ ...f, regex: v }))}
|
||||
/>
|
||||
<span>Use regex matching</span>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setOpen(false)} disabled={isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={isPending || !isValid}>
|
||||
{isPending ? 'Saving…' : isEdit ? 'Save Changes' : 'Create Rule'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardRulesSection({ site }: { site: Site }) {
|
||||
const { data: rules, isLoading } = useForwardRules(site.id);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-base">Forward Rules</CardTitle>
|
||||
<CardDescription>Redirect or rewrite incoming request paths.</CardDescription>
|
||||
</div>
|
||||
<ForwardRuleDialog
|
||||
siteId={site.id}
|
||||
trigger={
|
||||
<Button size="sm">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add Rule
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Loading…</p>}
|
||||
|
||||
{!isLoading && (!rules || rules.length === 0) && (
|
||||
<Empty>
|
||||
<EmptyHdr>
|
||||
<EmptyMedia variant="icon">
|
||||
<Route />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No forward rules</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Add a rule to redirect or rewrite request paths.
|
||||
</EmptyDescription>
|
||||
</EmptyHdr>
|
||||
</Empty>
|
||||
)}
|
||||
|
||||
{rules && rules.length > 0 && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Source</TableHead>
|
||||
<TableHead>Destination</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">Status</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">Regex</TableHead>
|
||||
<TableHead className="w-20" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rules.map((rule) => (
|
||||
<ForwardRuleRow key={rule.id} rule={rule} siteId={site.id} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function ForwardRuleRow({ rule, siteId }: { rule: ForwardRule; siteId: string }) {
|
||||
const deleteRule = useDeleteForwardRule(siteId);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-sm max-w-50 truncate">{rule.source}</TableCell>
|
||||
<TableCell className="font-mono text-sm max-w-50 truncate">
|
||||
{rule.destination}
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
<Badge variant="secondary">{rule.status_code}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden sm:table-cell">
|
||||
{rule.regex ? <Check className="h-4 w-4" /> : <X className="h-4 w-4" />}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<ForwardRuleDialog
|
||||
rule={rule}
|
||||
siteId={siteId}
|
||||
trigger={
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete forward rule?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will remove the redirect from{' '}
|
||||
<code className="text-sm">{rule.source}</code>. This action
|
||||
cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => deleteRule.mutate(rule.id)}
|
||||
disabled={deleteRule.isPending}
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
interface HeaderGroupFormData {
|
||||
source: string;
|
||||
regex: boolean;
|
||||
}
|
||||
|
||||
interface HeaderFormData {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function CustomHeadersSection({ site }: { site: Site }) {
|
||||
const { data: headerGroups, isLoading } = useCustomHeaders(site.id);
|
||||
const [groupOpen, setGroupOpen] = useState(false);
|
||||
const [groupForm, setGroupForm] = useState<HeaderGroupFormData>({ source: '', regex: false });
|
||||
const createGroup = useCreateCustomHeaders(site.id);
|
||||
|
||||
const handleCreateGroup = () => {
|
||||
createGroup.mutate(groupForm, {
|
||||
onSuccess: () => {
|
||||
setGroupOpen(false);
|
||||
setGroupForm({ source: '', regex: false });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-base">Custom Headers</CardTitle>
|
||||
<CardDescription>
|
||||
Attach custom response headers to matching paths.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Dialog open={groupOpen} onOpenChange={setGroupOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm">
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add Group
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>New Header Group</DialogTitle>
|
||||
<DialogDescription>
|
||||
Define a path pattern and then add headers to it.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="hg-source">Path pattern</Label>
|
||||
<Input
|
||||
id="hg-source"
|
||||
placeholder="/*"
|
||||
value={groupForm.source}
|
||||
onChange={(e) =>
|
||||
setGroupForm((f) => ({ ...f, source: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Label className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={groupForm.regex}
|
||||
onCheckedChange={(v) =>
|
||||
setGroupForm((f) => ({ ...f, regex: v }))
|
||||
}
|
||||
/>
|
||||
<span>Use regex matching</span>
|
||||
</Label>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setGroupOpen(false)}
|
||||
disabled={createGroup.isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateGroup}
|
||||
disabled={createGroup.isPending || groupForm.source.trim() === ''}
|
||||
>
|
||||
{createGroup.isPending ? 'Creating…' : 'Create Group'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading && <p className="text-sm text-muted-foreground">Loading…</p>}
|
||||
|
||||
{!isLoading && (!headerGroups || headerGroups.length === 0) && (
|
||||
<Empty>
|
||||
<EmptyHdr>
|
||||
<EmptyMedia variant="icon">
|
||||
<ShieldPlus />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No custom headers</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Add a header group to attach custom response headers to matching
|
||||
paths.
|
||||
</EmptyDescription>
|
||||
</EmptyHdr>
|
||||
</Empty>
|
||||
)}
|
||||
|
||||
{headerGroups && headerGroups.length > 0 && (
|
||||
<Accordion type="multiple" className="w-full">
|
||||
{headerGroups.map((group) => (
|
||||
<HeaderGroupItem key={group.id} group={group} siteId={site.id} />
|
||||
))}
|
||||
</Accordion>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderGroupItem({ group, siteId }: { group: CustomHeaders; siteId: string }) {
|
||||
const deleteGroup = useDeleteCustomHeaders(group.id, siteId);
|
||||
const createHeader = useCreateHeader(siteId, group.id);
|
||||
const [headerOpen, setHeaderOpen] = useState(false);
|
||||
const [headerForm, setHeaderForm] = useState<HeaderFormData>({ key: '', value: '' });
|
||||
|
||||
const handleCreateHeader = () => {
|
||||
createHeader.mutate(headerForm, {
|
||||
onSuccess: () => {
|
||||
setHeaderOpen(false);
|
||||
setHeaderForm({ key: '', value: '' });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<AccordionItem value={group.id}>
|
||||
<AccordionTrigger className="hover:no-underline">
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="text-sm font-mono">{group.source}</code>
|
||||
{group.regex && <Badge variant="outline">regex</Badge>}
|
||||
<Badge variant="secondary">
|
||||
{group.headers?.length ?? 0} header
|
||||
{(group.headers?.length ?? 0) !== 1 && 's'}
|
||||
</Badge>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-3 pt-1">
|
||||
{group.headers && group.headers.length > 0 && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Key</TableHead>
|
||||
<TableHead>Value</TableHead>
|
||||
<TableHead className="w-20" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{group.headers.map((header) => (
|
||||
<HeaderRow key={header.id} header={header} siteId={siteId} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
{(!group.headers || group.headers.length === 0) && (
|
||||
<p className="text-sm text-muted-foreground py-2">
|
||||
No headers yet. Add one below.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Dialog open={headerOpen} onOpenChange={setHeaderOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
<Plus className="h-3.5 w-3.5 mr-1" />
|
||||
Add Header
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>New Header</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a response header for{' '}
|
||||
<code className="text-sm">{group.source}</code>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-2">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="h-key">Header name</Label>
|
||||
<Input
|
||||
id="h-key"
|
||||
placeholder="X-Frame-Options"
|
||||
value={headerForm.key}
|
||||
onChange={(e) =>
|
||||
setHeaderForm((f) => ({
|
||||
...f,
|
||||
key: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="h-value">Header value</Label>
|
||||
<Input
|
||||
id="h-value"
|
||||
placeholder="DENY"
|
||||
value={headerForm.value}
|
||||
onChange={(e) =>
|
||||
setHeaderForm((f) => ({
|
||||
...f,
|
||||
value: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setHeaderOpen(false)}
|
||||
disabled={createHeader.isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateHeader}
|
||||
disabled={
|
||||
createHeader.isPending ||
|
||||
headerForm.key.trim() === '' ||
|
||||
headerForm.value.trim() === ''
|
||||
}
|
||||
>
|
||||
{createHeader.isPending ? 'Adding…' : 'Add Header'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="text-destructive">
|
||||
<Trash2 className="h-3.5 w-3.5 mr-1" />
|
||||
Delete Group
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete header group?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will remove all headers for{' '}
|
||||
<code className="text-sm">{group.source}</code>. This cannot
|
||||
be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => deleteGroup.mutate()}
|
||||
disabled={deleteGroup.isPending}
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
);
|
||||
}
|
||||
|
||||
function HeaderRow({ header, siteId }: { header: Header; siteId: string }) {
|
||||
const deleteHeader = useDeleteHeader(siteId, header.id);
|
||||
const updateHeader = useUpdateHeader(siteId, header.id);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [form, setForm] = useState<HeaderFormData>({ key: header.key, value: header.value });
|
||||
|
||||
const handleUpdate = () => {
|
||||
updateHeader.mutate(form, {
|
||||
onSuccess: () => setEditOpen(false),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-sm">{header.key}</TableCell>
|
||||
<TableCell className="font-mono text-sm max-w-62.5 truncate">{header.value}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1">
|
||||
<Dialog open={editOpen} onOpenChange={setEditOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Header</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-2">
|
||||
<div className="grid gap-2">
|
||||
<Label>Header name</Label>
|
||||
<Input
|
||||
value={form.key}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, key: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>Header value</Label>
|
||||
<Input
|
||||
value={form.value}
|
||||
onChange={(e) =>
|
||||
setForm((f) => ({ ...f, value: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setEditOpen(false)}
|
||||
disabled={updateHeader.isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleUpdate}
|
||||
disabled={
|
||||
updateHeader.isPending ||
|
||||
form.key.trim() === '' ||
|
||||
form.value.trim() === ''
|
||||
}
|
||||
>
|
||||
{updateHeader.isPending ? 'Saving…' : 'Save'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete header?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Remove <code className="text-sm">{header.key}</code>? This
|
||||
cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => deleteHeader.mutate()}
|
||||
disabled={deleteHeader.isPending}
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default function RulesTab({ site }: { site: Site }) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<ForwardRulesSection site={site} />
|
||||
<CustomHeadersSection site={site} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,15 +5,7 @@ import { Switch } from '@/components/ui/switch';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import {
|
||||
ArrowRightLeft,
|
||||
FileCode,
|
||||
Globe,
|
||||
LayoutDashboard,
|
||||
Rocket,
|
||||
Settings,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { Globe, LayoutDashboard, Rocket, Settings, X, Route } from 'lucide-react';
|
||||
import { useSite } from '../../hooks/api/useSite';
|
||||
import { useToggleSiteEnabled } from '../../hooks/api/useToggleSiteEnabled';
|
||||
import SettingsTab from './SettingsTab';
|
||||
@@ -28,14 +20,9 @@ import {
|
||||
EmptyTitle,
|
||||
} from '../../components/ui/empty';
|
||||
import { useDeploymentsForSite } from '../../hooks/api/useDeploymentsForSite';
|
||||
import RulesTab from './RulesTab';
|
||||
|
||||
const VALID_TABS = [
|
||||
'overview',
|
||||
'deployments',
|
||||
'forward-rules',
|
||||
'custom-headers',
|
||||
'settings',
|
||||
] as const;
|
||||
const VALID_TABS = ['overview', 'deployments', 'rules', 'settings'] as const;
|
||||
type TabValue = (typeof VALID_TABS)[number];
|
||||
|
||||
const DEFAULT_TAB: TabValue = 'overview';
|
||||
@@ -163,7 +150,7 @@ const SiteOverview = () => {
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center mb-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="overview">
|
||||
<LayoutDashboard />
|
||||
@@ -173,13 +160,9 @@ const SiteOverview = () => {
|
||||
<Rocket />
|
||||
Deployments
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="forward-rules">
|
||||
<ArrowRightLeft />
|
||||
Forward Rules
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="custom-headers">
|
||||
<FileCode />
|
||||
Custom Headers
|
||||
<TabsTrigger value="rules">
|
||||
<Route />
|
||||
Rules
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="settings">
|
||||
<Settings />
|
||||
@@ -206,6 +189,10 @@ const SiteOverview = () => {
|
||||
<DeploymentsTab site={site} deployments={deployments} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="rules">
|
||||
<RulesTab site={site} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="settings">
|
||||
<SettingsTab site={site} />
|
||||
</TabsContent>
|
||||
|
||||
Reference in New Issue
Block a user