Skip to content

Commit

Permalink
[feature] Allow API Namespace to auto clean resources (#1493)
Browse files Browse the repository at this point in the history
  • Loading branch information
donrestarone and Pralish authored Mar 25, 2023
1 parent fd1986b commit 489e7ab
Show file tree
Hide file tree
Showing 18 changed files with 583 additions and 223 deletions.
40 changes: 40 additions & 0 deletions app/assets/stylesheets/api_namespaces.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,46 @@
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: https://sass-lang.com/

.api-namespace-show {
background-color: #FCFEFF;

.nav-link-container {
overflow-x: auto;
background: #49505714;
padding: 12px;
-ms-overflow-style: none; /* Internet Explorer 10+ */
scrollbar-width: none;
margin: 30px 0;

.nav.nav-tabs {
width: max-content;
border-bottom: none;

.nav-link {
font-size: 14px;
color: #495057;
border: none;

&.active {
border: 1px solid #49505714;
box-shadow: 0px 8px 10px #0108150A;
border-radius: 4px;
color: #705AFE;
font-weight: 600;
}
}
}

&::-webkit-scrollbar {
display: none; /* Safari and Chrome */
}
}

.tab-content {
margin: 30px 0;
}
}

#api-resources-list{
a:not(.btn) {
color: #6F5AFE;
Expand Down
83 changes: 37 additions & 46 deletions app/controllers/comfy/admin/api_namespaces_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ class Comfy::Admin::ApiNamespacesController < Comfy::Admin::Cms::BaseController
before_action :ensure_authority_for_allow_exports_in_api, only: %i[ export export_api_resources export_without_associations_as_json export_with_associations_as_json ]
before_action :ensure_authority_for_allow_duplication_in_api, only: %i[ duplicate_with_associations duplicate_without_associations ]
before_action :ensure_authority_for_allow_social_share_metadata_in_api, only: %i[ social_share_metadata ]
before_action :ensure_authority_for_allow_settings_in_api, only: :update_settings
before_action :ensure_authority_to_manage_analytics, only: :analytics_metadata
before_action :ensure_authority_for_full_access_for_api_actions_only_in_api, only: %i[ api_action_workflow discard_failed_api_actions rerun_failed_api_actions ]
after_action :destroy_old_api_resources, only: :update_settings

# GET /api_namespaces or /api_namespaces.json
def index
Expand Down Expand Up @@ -84,21 +86,7 @@ def create

# PATCH/PUT /api_namespaces/1 or /api_namespaces/1.json
def update
respond_to do |format|
if @api_namespace.update(api_namespace_params)
format.html do
flash[:notice] = 'Api namespace was successfully updated.'
redirect_to @api_namespace
end
format.json { render :show, status: :ok, location: @api_namespace }
else
format.html do
flash[:error] = @api_namespace.errors.full_messages
render :edit, status: :unprocessable_entity
end
format.json { render json: @api_namespace.errors, status: :unprocessable_entity }
end
end
update_api_namespace(api_namespace_params, 'Api namespace was successfully updated.')
end

# DELETE /api_namespaces/1 or /api_namespaces/1.json
Expand Down Expand Up @@ -211,40 +199,15 @@ def import_as_json
end

def social_share_metadata
respond_to do |format|
if @api_namespace.update(api_namespace_social_share_metadata_params)
format.html do
flash[:notice] = 'Social Share Metadata successfully updated.'
redirect_to @api_namespace
end
format.json { render :show, status: :ok, location: @api_namespace }
else
format.html do
flash[:error] = @api_namespace.errors.full_messages
render :edit, status: :unprocessable_entity
end
format.json { render json: @api_namespace.errors, status: :unprocessable_entity }
end
end
update_api_namespace(api_namespace_social_share_metadata_params, 'Social Share Metadata successfully updated.')
end


def analytics_metadata
respond_to do |format|
if @api_namespace.update(analytics_metadata_params)
format.html do
flash[:notice] = 'Analytics Metadata successfully updated.'
redirect_to @api_namespace
end
format.json { render :show, status: :ok, location: @api_namespace }
else
format.html do
flash[:error] = @api_namespace.errors.full_messages
render :edit, status: :unprocessable_entity
end
format.json { render json: @api_namespace.errors, status: :unprocessable_entity }
end
end
update_api_namespace(analytics_metadata_params, 'Analytics Metadata successfully updated.')
end

def update_settings
update_api_namespace(api_namespace_settings_params, 'Api namespace setting was successfully updated.')
end

def api_action_workflow
Expand All @@ -266,6 +229,24 @@ def api_action_workflow
end

private
def update_api_namespace(permitted_params, success_response)
respond_to do |format|
if @api_namespace.update(permitted_params)
format.html do
flash[:success] = success_response
redirect_to @api_namespace
end
format.json { render :show, status: :ok, location: @api_namespace }
else
format.html do
flash[:danger] = @api_namespace.errors.full_messages
render :edit, status: :unprocessable_entity
end
format.json { render json: @api_namespace.errors, status: :unprocessable_entity }
end
end
end

# Use callbacks to share common setup or constraints between actions.
def set_api_namespace
@api_namespace = ApiNamespace.friendly.find(params[:id])
Expand Down Expand Up @@ -301,4 +282,14 @@ def api_namespace_social_share_metadata_params
def analytics_metadata_params
params.require(:api_namespace).permit(analytics_metadata: [:title, :author, :thumbnail])
end

def api_namespace_settings_params
params.require(:api_namespace).permit(:purge_resources_older_than)
end

def destroy_old_api_resources
return if @api_namespace.errors.present? || @api_namespace.purge_resources_older_than == ApiNamespace::RESOURCES_PURGE_INTERVAL_MAPPING[:never]

PurgeOldApiResourcesJob.perform_async(@api_namespace.id)
end
end
7 changes: 7 additions & 0 deletions app/controllers/subdomains/base_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ def ensure_authority_for_allow_social_share_metadata_in_api
end
end

def ensure_authority_for_allow_settings_in_api
unless user_authorized_for_api_accessibility?(ApiNamespace::API_ACCESSIBILITIES[:allow_settings])
flash[:danger] = "You do not have the permission to do that. Only users with full_access or full_access_api_namespace_only or allow_settings are allowed to perform that action."
redirect_back(fallback_location: root_url)
end
end

def ensure_authority_for_read_api_actions_only_in_api
unless user_authorized_for_api_accessibility?(ApiNamespace::API_ACCESSIBILITIES[:read_api_actions_only])
flash.alert = "You do not have the permission to do that. Only users with full_access or full_read_access or full_access_for_api_actions_only or read_api_actions_only are allowed to perform that action."
Expand Down
20 changes: 19 additions & 1 deletion app/models/api_namespace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,19 @@ class ApiNamespace < ApplicationRecord
Arel.sql("api_namespaces.properties::text")
end

RESOURCES_PURGE_INTERVAL_MAPPING = {
'1 week': '1.week',
'2 weeks': '2.weeks',
'1 month': '1.month',
'3 months': '3.months',
'6 months': '6.months',
'1 year': '1.year',
'never': 'never'
}

validates :purge_resources_older_than, inclusion: { in: RESOURCES_PURGE_INTERVAL_MAPPING.values,
message: "purge_resources_older_than is not valid" }, if: -> { attributes.key?('purge_resources_older_than') }

REGISTERED_PLUGINS = {
subdomain_events: {
slug: 'subdomain_events',
Expand All @@ -60,12 +73,13 @@ class ApiNamespace < ApplicationRecord
API_ACCESSIBILITIES = {
full_access: ['full_access'],
full_read_access: ['full_access', 'full_read_access'],
full_read_access_in_api_namespace: ['full_access', 'full_read_access', 'delete_access_api_namespace_only', 'allow_exports', 'allow_duplication', 'allow_social_share_metadata', 'full_access_api_namespace_only', 'read_api_resources_only', 'full_access_for_api_resources_only', 'delete_access_for_api_resources_only', 'read_api_actions_only', 'full_access_for_api_actions_only', 'read_external_api_connections_only', 'full_access_for_external_api_connections_only', 'read_api_clients_only', 'full_access_for_api_clients_only', 'full_access_for_api_form_only'],
full_read_access_in_api_namespace: ['full_access', 'full_read_access', 'delete_access_api_namespace_only', 'allow_exports', 'allow_duplication', 'allow_social_share_metadata', 'allow_settings', 'full_access_api_namespace_only', 'read_api_resources_only', 'full_access_for_api_resources_only', 'delete_access_for_api_resources_only', 'read_api_actions_only', 'full_access_for_api_actions_only', 'read_external_api_connections_only', 'full_access_for_external_api_connections_only', 'read_api_clients_only', 'full_access_for_api_clients_only', 'full_access_for_api_form_only'],
full_access_api_namespace_only: ['full_access', 'full_access_api_namespace_only'],
delete_access_api_namespace_only: ['full_access', 'full_access_api_namespace_only', 'delete_access_api_namespace_only'],
allow_exports: ['full_access', 'full_access_api_namespace_only', 'allow_exports'],
allow_duplication: ['full_access', 'full_access_api_namespace_only', 'allow_duplication'],
allow_social_share_metadata: ['full_access', 'full_access_api_namespace_only', 'allow_social_share_metadata'],
allow_settings: ['full_access', 'full_access_api_namespace_only', 'allow_settings'],
read_api_resources_only: ['full_access', 'full_read_access', 'full_access_for_api_resources_only', 'read_api_resources_only', 'delete_access_for_api_resources_only'],
full_access_for_api_resources_only: ['full_access', 'full_access_for_api_resources_only'],
delete_access_for_api_resources_only: ['full_access', 'full_access_for_api_resources_only', 'delete_access_for_api_resources_only'],
Expand Down Expand Up @@ -379,4 +393,8 @@ def cms_associations

associations.uniq
end

def destroy_old_api_resources_in_batches
api_resources.where("created_at < ?", eval("#{purge_resources_older_than}.ago")).in_batches(&:destroy_all)
end
end
8 changes: 8 additions & 0 deletions app/sidekiq/purge_old_api_resources_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class PurgeOldApiResourcesJob
include Sidekiq::Job

def perform(api_namespace_id)
api_namespace = ApiNamespace.find_by(id: api_namespace_id)
api_namespace&.destroy_old_api_resources_in_batches
end
end
59 changes: 59 additions & 0 deletions app/views/comfy/admin/api_namespaces/_interface.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
%div
%p
%b Requires authentication:
= api_namespace.requires_authentication
%p
%b Namespace type:
= api_namespace.namespace_type

.card.bg-white
.card-body
= render partial: 'rest_interface', locals: { api_namespace: api_namespace }

.card-body
%h4
Graph Interface
%strong
Request description endpoint:
%p
GET
%pre
= "#{graphql_base_url(Subdomain.current, api_namespace)}/describe"

%strong
Request query endpoint:
%p
POST
%pre
= "#{graphql_base_url(Subdomain.current, api_namespace)}"
%p
Payload (this)
%pre
= "query: { apiNamespaces(slug: \"#{api_namespace.slug}\") { id } }"
%p
Payload (this + children)
%pre
= "query: { apiNamespaces(slug: \"#{api_namespace.slug}\") { id apiResources { id } } }"
%p
Payload (global)
%pre
= "query: { apiNamespaces { id } }"

.card-body
%h4
Webhook
.table-responsive
%table
%thead
%tr
%th.px-3 API Connection
%th.px-3 HTTP method
%th.px-3 Webhook Interface
%th.px-3 Verification Method
%tbody
- api_namespace.external_api_clients.where(drive_strategy: ExternalApiClient::DRIVE_STRATEGIES[:webhook]).each do |external_api_client|
%tr
%td.px-3= link_to external_api_client.label, api_namespace_external_api_client_path(api_namespace_id: external_api_client.api_namespace.id, id: external_api_client.id)
%td.px-3 POST
%td.px-3= api_external_api_client_webhook_url(version: external_api_client.api_namespace.version, api_namespace: external_api_client.api_namespace.slug, external_api_client: external_api_client.slug)
%td.px-3= external_api_client.webhook_verification_method&.webhook_type
19 changes: 19 additions & 0 deletions app/views/comfy/admin/api_namespaces/_settings.html.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
- has_full_read_access = has_access_to_api_accessibility?(ApiNamespace::API_ACCESSIBILITIES[:full_read_access], current_user, api_namespace)
- has_allow_settings_access = has_access_to_api_accessibility?(ApiNamespace::API_ACCESSIBILITIES[:allow_settings], current_user, api_namespace)
- if has_allow_settings_access || has_full_read_access
.card.p-3.mb-4
= form_with(method: :patch, model: api_namespace, url: settings_api_namespace_path(api_namespace)) do |f|
.form-group
= f.label :purge_resources_older_than
= f.select :purge_resources_older_than, options_for_select(ApiNamespace::RESOURCES_PURGE_INTERVAL_MAPPING, api_namespace.purge_resources_older_than), { }, {class: 'form-control', disabled: !has_allow_settings_access}

- if has_allow_settings_access
.form-group.mb-0
= f.submit "Submit", class: 'btn btn-primary', data: { confirm: 'This will trigger a purge job that will delete all API resources older than the selected interval.' }

.my-3
= link_to 'Delete', api_namespace, method: :delete, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger'
.my-3
= link_to 'Duplicate with associations', duplicate_with_associations_api_namespace_path(id: api_namespace.id), method: :post, target: '_blank', class: 'btn btn-primary'
.my-3
= link_to 'Duplicate without associations', duplicate_without_associations_api_namespace_path(id: api_namespace.id), method: :post, target: '_blank', class: 'btn btn-primary'
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
- if has_access_to_api_accessibility?(ApiNamespace::API_ACCESSIBILITIES[:read_api_resources_only], current_user, @api_namespace)
#api-resources-list.mb-5{data: {controller: "api-resources"}}
.d-sm-flex.justify-content-between.align-items-center.py-2.page-header
%h2
Listing
= @api_namespace.name.pluralize
%div
= link_to "Create new #{@api_namespace.name.singularize}", new_api_namespace_resource_path(api_namespace_id: @api_namespace.id), class: 'btn btn-primary w-100 w-md-auto my-2'
= search_form_for @api_resources_q, url: api_namespace_path(@api_namespace.id), data: {api_resources_target: 'searchForm', turbo_frame: "api-resources", turbo_action: "advance"} do |f|
.form-group
= f.label "Search by properties", class: 'col-form-label'
= f.label "Search by properties", class: 'col-form-label pt-0'
.search-box
= f.search_field :properties_cont, value: params[:q][:properties_cont], class: 'form-control', placeholder: 'Enter the value of properties'
.form-group.mt-4.mb-2.d-flex.justify-content-between
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
.digg_pagination.d-sm-flex.justify-content-between.mb-4.pb-3
.page-info= page_entries_info @api_resources
.links= will_paginate @api_resources, container: false, renderer: TurboPaginateRenderer
.table-responsive
.table-responsive.bg-white
%div.api-resources-table-container
%table#api-resources-table.table.table-striped.table-bordered
%thead
Expand Down
Loading

0 comments on commit 489e7ab

Please sign in to comment.