Add frontend #1
@@ -175,22 +175,26 @@ func (c *CachedSiteRepository) CreateForwardRule(siteID string, fr *models.Forwa
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CachedSiteRepository) UpdateForwardRule(fr *models.ForwardRule) error {
|
func (c *CachedSiteRepository) UpdateForwardRule(siteID string, fr *models.ForwardRule) error {
|
||||||
if err := c.inner.UpdateForwardRule(fr); err != nil {
|
if err := c.inner.UpdateForwardRule(siteID, fr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
c.forwardRules[fr.ID] = fr
|
c.forwardRules[fr.ID] = fr
|
||||||
|
delete(c.sites, siteID)
|
||||||
|
c.siteListValid = false
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CachedSiteRepository) DeleteForwardRule(id string) error {
|
func (c *CachedSiteRepository) DeleteForwardRule(siteID string, id string) error {
|
||||||
if err := c.inner.DeleteForwardRule(id); err != nil {
|
if err := c.inner.DeleteForwardRule(siteID, id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
delete(c.forwardRules, id)
|
delete(c.forwardRules, id)
|
||||||
|
delete(c.sites, siteID)
|
||||||
|
c.siteListValid = false
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -228,22 +232,26 @@ func (c *CachedSiteRepository) CreateCustomHeaders(siteID string, ch *models.Cus
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CachedSiteRepository) UpdateCustomHeaders(ch *models.CustomHeaders) error {
|
func (c *CachedSiteRepository) UpdateCustomHeaders(siteID string, ch *models.CustomHeaders) error {
|
||||||
if err := c.inner.UpdateCustomHeaders(ch); err != nil {
|
if err := c.inner.UpdateCustomHeaders(siteID, ch); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
c.customHeaders[ch.ID] = ch
|
c.customHeaders[ch.ID] = ch
|
||||||
|
delete(c.sites, siteID)
|
||||||
|
c.siteListValid = false
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CachedSiteRepository) DeleteCustomHeaders(id string) error {
|
func (c *CachedSiteRepository) DeleteCustomHeaders(siteID string, id string) error {
|
||||||
if err := c.inner.DeleteCustomHeaders(id); err != nil {
|
if err := c.inner.DeleteCustomHeaders(siteID, id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
delete(c.customHeaders, id)
|
delete(c.customHeaders, id)
|
||||||
|
delete(c.sites, siteID)
|
||||||
|
c.siteListValid = false
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -269,33 +277,39 @@ func (c *CachedSiteRepository) GetHeader(id string) (*models.Header, error) {
|
|||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CachedSiteRepository) CreateHeader(customHeaderID string, h *models.Header) error {
|
func (c *CachedSiteRepository) CreateHeader(siteID string, customHeaderID string, h *models.Header) error {
|
||||||
if err := c.inner.CreateHeader(customHeaderID, h); err != nil {
|
if err := c.inner.CreateHeader(siteID, customHeaderID, h); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
c.headers[h.ID] = h
|
c.headers[h.ID] = h
|
||||||
delete(c.customHeaders, customHeaderID)
|
delete(c.customHeaders, customHeaderID)
|
||||||
|
delete(c.sites, siteID)
|
||||||
|
c.siteListValid = false
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CachedSiteRepository) UpdateHeader(h *models.Header) error {
|
func (c *CachedSiteRepository) UpdateHeader(siteID string, h *models.Header) error {
|
||||||
if err := c.inner.UpdateHeader(h); err != nil {
|
if err := c.inner.UpdateHeader(siteID, h); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
c.headers[h.ID] = h
|
c.headers[h.ID] = h
|
||||||
|
delete(c.sites, siteID)
|
||||||
|
c.siteListValid = false
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CachedSiteRepository) DeleteHeader(id string) error {
|
func (c *CachedSiteRepository) DeleteHeader(siteID string, id string) error {
|
||||||
if err := c.inner.DeleteHeader(id); err != nil {
|
if err := c.inner.DeleteHeader(siteID, id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
delete(c.headers, id)
|
delete(c.headers, id)
|
||||||
|
delete(c.sites, siteID)
|
||||||
|
c.siteListValid = false
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func (h *SiteHandler) GetSiteForwardRules(c fiber.Ctx) error {
|
|||||||
// @Failure 500 {object} models.APIError "Internal server error"
|
// @Failure 500 {object} models.APIError "Internal server error"
|
||||||
// @Router /forward-rules/{id} [get]
|
// @Router /forward-rules/{id} [get]
|
||||||
func (h *SiteHandler) GetForwardRule(c fiber.Ctx) error {
|
func (h *SiteHandler) GetForwardRule(c fiber.Ctx) error {
|
||||||
id := c.Params("id")
|
id := c.Params("ruleId")
|
||||||
|
|
||||||
rule, err := h.Repo.GetForwardRule(id)
|
rule, err := h.Repo.GetForwardRule(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -169,7 +169,8 @@ func (h *SiteHandler) PostForwardRule(c fiber.Ctx) error {
|
|||||||
// @Failure 500 {object} models.APIError "Internal server error"
|
// @Failure 500 {object} models.APIError "Internal server error"
|
||||||
// @Router /forward-rules/{id} [put]
|
// @Router /forward-rules/{id} [put]
|
||||||
func (h *SiteHandler) PutForwardRule(c fiber.Ctx) error {
|
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 _, err := h.Repo.GetForwardRule(id); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
@@ -190,7 +191,7 @@ func (h *SiteHandler) PutForwardRule(c fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rule.ID = id
|
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)
|
log.Println("Error updating forward rule: ", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating forward rule"})
|
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"
|
// @Failure 500 {object} models.APIError "Internal server error"
|
||||||
// @Router /forward-rules/{id} [delete]
|
// @Router /forward-rules/{id} [delete]
|
||||||
func (h *SiteHandler) DeleteForwardRule(c fiber.Ctx) error {
|
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 _, err := h.Repo.GetForwardRule(id); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
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"})
|
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)
|
log.Println("Error deleting forward rule: ", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting forward rule"})
|
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"
|
// @Failure 500 {object} models.APIError "Internal server error"
|
||||||
// @Router /custom-headers/{id} [get]
|
// @Router /custom-headers/{id} [get]
|
||||||
func (h *SiteHandler) GetCustomHeaders(c fiber.Ctx) error {
|
func (h *SiteHandler) GetCustomHeaders(c fiber.Ctx) error {
|
||||||
id := c.Params("id")
|
id := c.Params("customHeaderId")
|
||||||
|
|
||||||
customHeaders, err := h.Repo.GetCustomHeaders(id)
|
customHeaders, err := h.Repo.GetCustomHeaders(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -347,7 +349,8 @@ func (h *SiteHandler) PostCustomHeaders(c fiber.Ctx) error {
|
|||||||
// @Failure 500 {object} models.APIError "Internal server error"
|
// @Failure 500 {object} models.APIError "Internal server error"
|
||||||
// @Router /custom-headers/{id} [put]
|
// @Router /custom-headers/{id} [put]
|
||||||
func (h *SiteHandler) PutCustomHeaders(c fiber.Ctx) error {
|
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 _, err := h.Repo.GetCustomHeaders(id); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
@@ -368,7 +371,7 @@ func (h *SiteHandler) PutCustomHeaders(c fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
customHeaders.ID = id
|
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)
|
log.Println("Error updating custom headers: ", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating custom headers"})
|
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"
|
// @Failure 500 {object} models.APIError "Internal server error"
|
||||||
// @Router /custom-headers/{id} [delete]
|
// @Router /custom-headers/{id} [delete]
|
||||||
func (h *SiteHandler) DeleteCustomHeaders(c fiber.Ctx) error {
|
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 _, err := h.Repo.GetCustomHeaders(id); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
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"})
|
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)
|
log.Println("Error deleting custom headers: ", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting custom headers"})
|
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"
|
// @Failure 500 {object} models.APIError "Internal server error"
|
||||||
// @Router /custom-headers/{id}/headers [get]
|
// @Router /custom-headers/{id}/headers [get]
|
||||||
func (h *SiteHandler) GetCustomHeaderHeaders(c fiber.Ctx) error {
|
func (h *SiteHandler) GetCustomHeaderHeaders(c fiber.Ctx) error {
|
||||||
customHeaderID := c.Params("id")
|
customHeaderID := c.Params("customHeaderId")
|
||||||
|
|
||||||
customHeaders, err := h.Repo.GetCustomHeaders(customHeaderID)
|
customHeaders, err := h.Repo.GetCustomHeaders(customHeaderID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -453,7 +457,7 @@ func (h *SiteHandler) GetCustomHeaderHeaders(c fiber.Ctx) error {
|
|||||||
// @Failure 500 {object} models.APIError "Internal server error"
|
// @Failure 500 {object} models.APIError "Internal server error"
|
||||||
// @Router /headers/{id} [get]
|
// @Router /headers/{id} [get]
|
||||||
func (h *SiteHandler) GetHeader(c fiber.Ctx) error {
|
func (h *SiteHandler) GetHeader(c fiber.Ctx) error {
|
||||||
id := c.Params("id")
|
id := c.Params("headerId")
|
||||||
|
|
||||||
header, err := h.Repo.GetHeader(id)
|
header, err := h.Repo.GetHeader(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -482,7 +486,8 @@ func (h *SiteHandler) GetHeader(c fiber.Ctx) error {
|
|||||||
// @Failure 500 {object} models.APIError "Internal server error"
|
// @Failure 500 {object} models.APIError "Internal server error"
|
||||||
// @Router /custom-headers/{id}/headers [post]
|
// @Router /custom-headers/{id}/headers [post]
|
||||||
func (h *SiteHandler) PostHeader(c fiber.Ctx) error {
|
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 _, err := h.Repo.GetCustomHeaders(customHeaderID); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
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()})
|
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)
|
log.Println("Error creating header: ", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while creating header"})
|
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"
|
// @Failure 500 {object} models.APIError "Internal server error"
|
||||||
// @Router /headers/{id} [put]
|
// @Router /headers/{id} [put]
|
||||||
func (h *SiteHandler) PutHeader(c fiber.Ctx) error {
|
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 _, err := h.Repo.GetHeader(id); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
@@ -546,7 +552,7 @@ func (h *SiteHandler) PutHeader(c fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
header.ID = id
|
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)
|
log.Println("Error updating header: ", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while updating header"})
|
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"
|
// @Failure 500 {object} models.APIError "Internal server error"
|
||||||
// @Router /headers/{id} [delete]
|
// @Router /headers/{id} [delete]
|
||||||
func (h *SiteHandler) DeleteHeader(c fiber.Ctx) error {
|
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 _, err := h.Repo.GetHeader(id); err != nil {
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
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"})
|
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)
|
log.Println("Error deleting header: ", err)
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(&models.APIError{Message: "Unexpected error while deleting header"})
|
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
|
DeleteSite(id string) error
|
||||||
GetForwardRule(id string) (*models.ForwardRule, error)
|
GetForwardRule(id string) (*models.ForwardRule, error)
|
||||||
CreateForwardRule(siteID string, fr *models.ForwardRule) error
|
CreateForwardRule(siteID string, fr *models.ForwardRule) error
|
||||||
UpdateForwardRule(fr *models.ForwardRule) error
|
UpdateForwardRule(siteID string, fr *models.ForwardRule) error
|
||||||
DeleteForwardRule(id string) error
|
DeleteForwardRule(siteID string, id string) error
|
||||||
GetCustomHeaders(id string) (*models.CustomHeaders, error)
|
GetCustomHeaders(id string) (*models.CustomHeaders, error)
|
||||||
CreateCustomHeaders(siteID string, ch *models.CustomHeaders) error
|
CreateCustomHeaders(siteID string, ch *models.CustomHeaders) error
|
||||||
UpdateCustomHeaders(ch *models.CustomHeaders) error
|
UpdateCustomHeaders(siteID string, ch *models.CustomHeaders) error
|
||||||
DeleteCustomHeaders(id string) error
|
DeleteCustomHeaders(siteID string, id string) error
|
||||||
GetHeader(id string) (*models.Header, error)
|
GetHeader(id string) (*models.Header, error)
|
||||||
CreateHeader(customHeaderID string, h *models.Header) error
|
CreateHeader(siteID string, customHeaderID string, h *models.Header) error
|
||||||
UpdateHeader(h *models.Header) error
|
UpdateHeader(siteID string, h *models.Header) error
|
||||||
DeleteHeader(id string) 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
|
// Forward rules
|
||||||
api.Get("/sites/:id/forward-rules", siteHandler.GetSiteForwardRules)
|
api.Get("/sites/:id/forward-rules", siteHandler.GetSiteForwardRules)
|
||||||
api.Post("/sites/:id/forward-rules", siteHandler.PostForwardRule)
|
api.Post("/sites/:id/forward-rules", siteHandler.PostForwardRule)
|
||||||
api.Get("/forward-rules/:id", siteHandler.GetForwardRule)
|
api.Get("/sites/:id/forward-rules/:ruleId", siteHandler.GetForwardRule)
|
||||||
api.Put("/forward-rules/:id", siteHandler.PutForwardRule)
|
api.Put("/sites/:id/forward-rules/:ruleId", siteHandler.PutForwardRule)
|
||||||
api.Delete("/forward-rules/:id", siteHandler.DeleteForwardRule)
|
api.Delete("/sites/:id/forward-rules/:ruleId", siteHandler.DeleteForwardRule)
|
||||||
|
|
||||||
// Custom headers (header rules)
|
// Custom headers (header rules)
|
||||||
api.Get("/sites/:id/custom-headers", siteHandler.GetSiteCustomHeaders)
|
api.Get("/sites/:id/custom-headers", siteHandler.GetSiteCustomHeaders)
|
||||||
api.Post("/sites/:id/custom-headers", siteHandler.PostCustomHeaders)
|
api.Post("/sites/:id/custom-headers", siteHandler.PostCustomHeaders)
|
||||||
api.Get("/custom-headers/:id", siteHandler.GetCustomHeaders)
|
api.Get("/sites/:id/custom-headers/:customHeaderId", siteHandler.GetCustomHeaders)
|
||||||
api.Put("/custom-headers/:id", siteHandler.PutCustomHeaders)
|
api.Put("/sites/:id/custom-headers/:customHeaderId", siteHandler.PutCustomHeaders)
|
||||||
api.Delete("/custom-headers/:id", siteHandler.DeleteCustomHeaders)
|
api.Delete("/sites/:id/custom-headers/:customHeaderId", siteHandler.DeleteCustomHeaders)
|
||||||
|
|
||||||
// Headers
|
// Headers
|
||||||
api.Get("/custom-headers/:id/headers", siteHandler.GetCustomHeaderHeaders)
|
api.Get("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.GetCustomHeaderHeaders)
|
||||||
api.Post("/custom-headers/:id/headers", siteHandler.PostHeader)
|
api.Post("/sites/:id/custom-headers/:customHeaderId/headers", siteHandler.PostHeader)
|
||||||
api.Get("/headers/:id", siteHandler.GetHeader)
|
api.Get("/sites/:id/headers/:headerId", siteHandler.GetHeader)
|
||||||
api.Put("/headers/:id", siteHandler.PutHeader)
|
api.Put("/sites/:id/headers/:headerId", siteHandler.PutHeader)
|
||||||
api.Delete("/headers/:id", siteHandler.DeleteHeader)
|
api.Delete("/sites/:id/headers/:headerId", siteHandler.DeleteHeader)
|
||||||
|
|
||||||
// Deployments
|
// Deployments
|
||||||
api.Get("/deployments/:id", deploymentsHandler.GetDeployment)
|
api.Get("/deployments/:id", deploymentsHandler.GetDeployment)
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ func (r *SQLiteSiteRepository) CreateForwardRule(siteID string, fr *models.Forwa
|
|||||||
return tx.Commit()
|
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(`
|
_, err := r.db.Exec(`
|
||||||
UPDATE forward_rules SET source=?, destination=?, status_code=?, regex=? WHERE id=?`,
|
UPDATE forward_rules SET source=?, destination=?, status_code=?, regex=? WHERE id=?`,
|
||||||
fr.Source, fr.Destination, fr.StatusCode, fr.Regex, fr.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
|
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)
|
_, err := r.db.Exec(`DELETE FROM forward_rules WHERE id = ?`, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("delete forward rule: %w", err)
|
return fmt.Errorf("delete forward rule: %w", err)
|
||||||
@@ -284,7 +284,7 @@ func (r *SQLiteSiteRepository) CreateCustomHeaders(siteID string, ch *models.Cus
|
|||||||
return tx.Commit()
|
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()
|
tx, err := r.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("update custom headers begin tx: %w", err)
|
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()
|
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)
|
_, err := r.db.Exec(`DELETE FROM custom_headers WHERE id = ?`, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("delete custom headers: %w", err)
|
return fmt.Errorf("delete custom headers: %w", err)
|
||||||
@@ -325,7 +325,7 @@ func (r *SQLiteSiteRepository) GetHeader(id string) (*models.Header, error) {
|
|||||||
return &h, nil
|
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()
|
h.ID = uuid.NewString()
|
||||||
_, err := r.db.Exec(
|
_, err := r.db.Exec(
|
||||||
`INSERT INTO headers (id, custom_header_id, key, value) VALUES (?, ?, ?, ?)`,
|
`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
|
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)
|
_, err := r.db.Exec(`UPDATE headers SET key=?, value=? WHERE id=?`, h.Key, h.Value, h.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("update header: %w", err)
|
return fmt.Errorf("update header: %w", err)
|
||||||
@@ -345,7 +345,7 @@ func (r *SQLiteSiteRepository) UpdateHeader(h *models.Header) error {
|
|||||||
return nil
|
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)
|
_, err := r.db.Exec(`DELETE FROM headers WHERE id = ?`, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("delete header: %w", err)
|
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 {
|
export interface ToggleSiteEnabledResponse {
|
||||||
enabled: boolean;
|
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 { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import {
|
import { Globe, LayoutDashboard, Rocket, Settings, X, Route } from 'lucide-react';
|
||||||
ArrowRightLeft,
|
|
||||||
FileCode,
|
|
||||||
Globe,
|
|
||||||
LayoutDashboard,
|
|
||||||
Rocket,
|
|
||||||
Settings,
|
|
||||||
X,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { useSite } from '../../hooks/api/useSite';
|
import { useSite } from '../../hooks/api/useSite';
|
||||||
import { useToggleSiteEnabled } from '../../hooks/api/useToggleSiteEnabled';
|
import { useToggleSiteEnabled } from '../../hooks/api/useToggleSiteEnabled';
|
||||||
import SettingsTab from './SettingsTab';
|
import SettingsTab from './SettingsTab';
|
||||||
@@ -28,14 +20,9 @@ import {
|
|||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from '../../components/ui/empty';
|
} from '../../components/ui/empty';
|
||||||
import { useDeploymentsForSite } from '../../hooks/api/useDeploymentsForSite';
|
import { useDeploymentsForSite } from '../../hooks/api/useDeploymentsForSite';
|
||||||
|
import RulesTab from './RulesTab';
|
||||||
|
|
||||||
const VALID_TABS = [
|
const VALID_TABS = ['overview', 'deployments', 'rules', 'settings'] as const;
|
||||||
'overview',
|
|
||||||
'deployments',
|
|
||||||
'forward-rules',
|
|
||||||
'custom-headers',
|
|
||||||
'settings',
|
|
||||||
] as const;
|
|
||||||
type TabValue = (typeof VALID_TABS)[number];
|
type TabValue = (typeof VALID_TABS)[number];
|
||||||
|
|
||||||
const DEFAULT_TAB: TabValue = 'overview';
|
const DEFAULT_TAB: TabValue = 'overview';
|
||||||
@@ -163,7 +150,7 @@ const SiteOverview = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center mb-4">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="overview">
|
<TabsTrigger value="overview">
|
||||||
<LayoutDashboard />
|
<LayoutDashboard />
|
||||||
@@ -173,13 +160,9 @@ const SiteOverview = () => {
|
|||||||
<Rocket />
|
<Rocket />
|
||||||
Deployments
|
Deployments
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="forward-rules">
|
<TabsTrigger value="rules">
|
||||||
<ArrowRightLeft />
|
<Route />
|
||||||
Forward Rules
|
Rules
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="custom-headers">
|
|
||||||
<FileCode />
|
|
||||||
Custom Headers
|
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="settings">
|
<TabsTrigger value="settings">
|
||||||
<Settings />
|
<Settings />
|
||||||
@@ -206,6 +189,10 @@ const SiteOverview = () => {
|
|||||||
<DeploymentsTab site={site} deployments={deployments} />
|
<DeploymentsTab site={site} deployments={deployments} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="rules">
|
||||||
|
<RulesTab site={site} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="settings">
|
<TabsContent value="settings">
|
||||||
<SettingsTab site={site} />
|
<SettingsTab site={site} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user