Skip to content

Commit

Permalink
Merge pull request #316 from kortirso/issue_313
Browse files Browse the repository at this point in the history
IS-313 Fetching insights by API
  • Loading branch information
kortirso committed Jul 4, 2024
2 parents 082c9b3 + edbfbfe commit 0a5fae1
Show file tree
Hide file tree
Showing 27 changed files with 910 additions and 21 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- co-owners for accounts
- expiration time for access tokens
- creating/removing API access tokens
- API documentation
- fetching insights by API

### Modified
- skip reseting invites email after accepting invite
Expand Down
5 changes: 5 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,14 @@ gem 'active_delivery'
# antibot captcha
gem 'recaptcha', require: 'recaptcha/rails'

# API documentation
gem 'rswag-api'
gem 'rswag-ui'

group :development, :test do
gem 'bullet', git: 'https://github.com/flyerhzm/bullet', branch: 'main'
gem 'cypress-on-rails', '~> 1.0'
gem 'rswag-specs'
gem 'rubocop', '~> 1.35', require: false
gem 'rubocop-factory_bot', '~> 2.0', require: false
gem 'rubocop-performance', '~> 1.14', require: false
Expand Down
16 changes: 16 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ GEM
jsbundling-rails (1.3.0)
railties (>= 6.0.0)
json (2.7.2)
json-schema (4.3.0)
addressable (>= 2.8)
json_spec (1.1.5)
multi_json (~> 1.0)
rspec (>= 2.0, < 4.0)
Expand Down Expand Up @@ -378,6 +380,17 @@ GEM
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.1)
rswag-api (2.13.0)
activesupport (>= 3.1, < 7.2)
railties (>= 3.1, < 7.2)
rswag-specs (2.13.0)
activesupport (>= 3.1, < 7.2)
json-schema (>= 2.2, < 5.0)
railties (>= 3.1, < 7.2)
rspec-core (>= 2.14)
rswag-ui (2.13.0)
actionpack (>= 3.1, < 7.2)
railties (>= 3.1, < 7.2)
rubocop (1.64.1)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
Expand Down Expand Up @@ -520,6 +533,9 @@ DEPENDENCIES
redis-rack!
redis-rails!
rspec-rails (~> 6.0)
rswag-api
rswag-specs
rswag-ui
rubocop (~> 1.35)
rubocop-factory_bot (~> 2.0)
rubocop-performance (~> 1.14)
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ $ yarn run cypress run --project ./spec/e2e
$ bearer scan .
```

## API

API documentation is available at [api-docs](https://pullkeeper.dev/api-docs).

### Refresh API documentation

```bash
$ rails rswag:specs:swaggerize
```

## Application layers

contracts - model schemas for validators
Expand Down
5 changes: 3 additions & 2 deletions app/controllers/api/frontend/insights_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ def index
ratio_type: ratio_type
}
}
).serializable_hash
}, status: :ok
).serializable_hash,
ratio_type: ratio_enabled? ? ratio_type : nil
}.compact, status: :ok
end

private
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ def index
no_entity: true
}
}
).serializable_hash
}, status: :ok
).serializable_hash,
ratio_type: ratio_enabled? ? ratio_type : nil
}.compact, status: :ok
end

private
Expand Down
75 changes: 75 additions & 0 deletions app/controllers/api/v1/insights_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

module Api
module V1
class InsightsController < Api::V1Controller
before_action :find_insightable

def index
render json: {
insights: InsightSerializer.new(
actual_insights,
{
params: {
previous_insights: previous_insights,
insight_fields: insight_fields,
ratio_enabled: ratio_enabled?,
ratio_type: ratio_type
}
}
).serializable_hash,
ratio_type: ratio_enabled? ? ratio_type : nil
}.compact, status: :ok
end

private

def find_insightable
find_company if params[:company_id]
find_repository if params[:repository_id]

page_not_found unless @insightable
end

def find_company
@insightable = authorized_scope(Company.order(id: :desc)).find_by(uuid: params[:company_id])
end

def find_repository
@insightable = authorized_scope(Repository.order(id: :desc)).find_by(uuid: params[:repository_id])
end

def visible_insights
@visible_insights ||=
Insights::VisibleQuery
.new(relation: @insightable.insights)
.resolve(insightable: @insightable)
.load
end

def actual_insights
visible_insights.select(&:actual?)
end

def previous_insights
visible_insights.reject(&:actual?)
end

def insight_fields
if @insightable.premium? && @insightable.configuration.insight_fields.present?
@insightable.selected_insight_fields
else
Insight::DEFAULT_ATTRIBUTES
end
end

def ratio_enabled?
@insightable.premium? && @insightable.configuration.insight_ratio
end

def ratio_type
@insightable.configuration.insight_ratio_type
end
end
end
end
6 changes: 3 additions & 3 deletions app/javascript/components/Company/Company.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ export const Company = ({
const fetchInsights = async () => await insightsRequest(uuid);

Promise.all([fetchInsights()]).then(([insightsData]) => {
const insightTypes = insightsData.length > 0 ? Object.keys(insightsData[0].values) : [];
const ratioType = insightsData.length > 0 ? insightsData[0].ratio_type : null;
const insightTypes = insightsData.data.length > 0 ? Object.keys(insightsData.data[0].values) : [];
const ratioType = insightsData.ratioType || null;
setPageState({
...pageState,
entities: insightsData,
entities: insightsData.data,
insightTypes: insightTypes,
ratioType: ratioType,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ export const insightsRequest = async (companyUuid) => {
const result = await apiRequest({
url: `/api/frontend/companies/${companyUuid}/insights.json`,
});
return result.insights.data.map((element) => element.attributes);
return {
data: result.insights.data.map((element) => element.attributes),
ratioType: result.insights.ratio_type
};
};
6 changes: 3 additions & 3 deletions app/javascript/components/Repository/Repository.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ export const Repository = ({

Promise.all([fetchInsights(), fetchRepositoryInsights()]).then(
([insightsData, repositoryInsightsData]) => {
const insightTypes = insightsData.length > 0 ? Object.keys(insightsData[0].values) : [];
const ratioType = insightsData.length > 0 ? insightsData[0].ratio_type : null;
const insightTypes = insightsData.data.length > 0 ? Object.keys(insightsData.data[0].values) : [];
const ratioType = insightsData.ratioType || null;
setPageState({
...pageState,
entities: insightsData,
entities: insightsData.data,
insights:
Object.keys(repositoryInsightsData).length === 0 ? undefined : repositoryInsightsData,
insightTypes: insightTypes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ export const insightsRequest = async (repositoryUuid) => {
const result = await apiRequest({
url: `/api/frontend/repositories/${repositoryUuid}/insights.json`,
});
return result.insights.data.map((element) => element.attributes);
return {
data: result.insights.data.map((element) => element.attributes),
ratioType: result.insights.ratio_type
};
};
6 changes: 1 addition & 5 deletions app/serializers/insight_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,10 @@ class InsightSerializer < ApplicationSerializer
{
value: Insight::DECIMAL_ATTRIBUTES.include?(insight_field.to_sym) ? value.to_f : value,
ratio_value: params[:ratio_enabled] ? compare_with_previous_period(object, insight_field, params) : nil
}
}.compact
end
end

attribute :ratio_type do |_, params|
params[:ratio_enabled] ? params[:ratio_type] : nil
end

attribute :entity do |object, params|
params[:no_entity] ? nil : (Rails.cache.read("entity_payload_#{object.entity_id}_v1") || Entity::EMPTY_PAYLOAD)
end
Expand Down
1 change: 1 addition & 0 deletions app/views/components/page_wrappers/user_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<%= react_component 'FeedbackForm', email: Current.user.email, children: ("<span class='user-navigation-link cursor-pointer'>Feedback</span>").html_safe %>
<% cache('user_navigation_bottom_v3', expires_in: 24.hours) do %>
<%= link_to 'Metrics', metrics_path, class: 'user-navigation-link' %>
<a href="/api-docs" rel="noopener noreferrer" class="user-navigation-link">API docs</a>
<%= link_to 'Privacy policy', privacy_path, class: 'user-navigation-link' %>
<%= link_to t('components.page_wrappers.page_component.logout'), logout_path, class: 'user-navigation-link' %>
<% end %>
Expand Down
1 change: 1 addition & 0 deletions app/views/controllers/shared/_footer.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<%= react_component 'FeedbackForm', email: Current.user.email, children: ("<span class='cursor-pointer'>Feedback</span>").html_safe %>
<% end %>
<%= link_to 'Metrics', metrics_path, data: { 'test-id' => 'footer-metrics-link' } %>
<a href="/api-docs" rel="noopener noreferrer">API docs</a>
<%= link_to 'Privacy policy', privacy_path, data: { 'test-id' => 'footer-privacy-link' } %>
</div>
</footer>
4 changes: 2 additions & 2 deletions app/views/controllers/welcome/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
<p class="benefits-text">Compareable data with previous periods for better understanding development improvements and progress/regress of coding</p>
</div>
<div class="benefits-box">
<h3 class="benefits-title">Slack/Discord/Telegram notification</h3>
<p class="benefits-text">Receive notifications to Slack/Discord/Telegram with latest insights of your companies and repositories, or send data to custom url</p>
<h3 class="benefits-title">Slack/Discord/Telegram notifications</h3>
<p class="benefits-text">Receive notifications to Slack/Discord/Telegram with latest insights of your companies and repositories, or send data to custom url, or fetch data by <a href="https://pullkeeper.dev/api-docs" target="_blank" rel="noopener noreferrer" class="simple-link">API</a></p>
</div>
<div class="benefits-box">
<h3 class="benefits-title">Github and Gitlab API</h3>
Expand Down
116 changes: 116 additions & 0 deletions app/views/rswag/ui/home/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>PullKeeper API</title>
<link href="https://fonts.googleapis.com/css?family=Open+Sans:400,700|Source+Code+Pro:300,600|Titillium+Web:400,600,700" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
<link rel="icon" type="image/png" href="/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}

body {
margin:0;
background: #fafafa;
}
</style>
</head>

<body>

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position:absolute;width:0;height:0">
<defs>
<symbol viewBox="0 0 20 20" id="unlocked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V6h2v-.801C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8z"></path>
</symbol>

<symbol viewBox="0 0 20 20" id="locked">
<path d="M15.8 8H14V5.6C14 2.703 12.665 1 10 1 7.334 1 6 2.703 6 5.6V8H4c-.553 0-1 .646-1 1.199V17c0 .549.428 1.139.951 1.307l1.197.387C5.672 18.861 6.55 19 7.1 19h5.8c.549 0 1.428-.139 1.951-.307l1.196-.387c.524-.167.953-.757.953-1.306V9.199C17 8.646 16.352 8 15.8 8zM12 8H8V5.199C8 3.754 8.797 3 10 3c1.203 0 2 .754 2 2.199V8z"/>
</symbol>

<symbol viewBox="0 0 20 20" id="close">
<path d="M14.348 14.849c-.469.469-1.229.469-1.697 0L10 11.819l-2.651 3.029c-.469.469-1.229.469-1.697 0-.469-.469-.469-1.229 0-1.697l2.758-3.15-2.759-3.152c-.469-.469-.469-1.228 0-1.697.469-.469 1.228-.469 1.697 0L10 8.183l2.651-3.031c.469-.469 1.228-.469 1.697 0 .469.469.469 1.229 0 1.697l-2.758 3.152 2.758 3.15c.469.469.469 1.229 0 1.698z"/>
</symbol>

<symbol viewBox="0 0 20 20" id="large-arrow">
<path d="M13.25 10L6.109 2.58c-.268-.27-.268-.707 0-.979.268-.27.701-.27.969 0l7.83 7.908c.268.271.268.709 0 .979l-7.83 7.908c-.268.271-.701.27-.969 0-.268-.269-.268-.707 0-.979L13.25 10z"/>
</symbol>

<symbol viewBox="0 0 20 20" id="large-arrow-down">
<path d="M17.418 6.109c.272-.268.709-.268.979 0s.271.701 0 .969l-7.908 7.83c-.27.268-.707.268-.979 0l-7.908-7.83c-.27-.268-.27-.701 0-.969.271-.268.709-.268.979 0L10 13.25l7.418-7.141z"/>
</symbol>


<symbol viewBox="0 0 24 24" id="jump-to">
<path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.41L5.83 13H21V7z"/>
</symbol>

<symbol viewBox="0 0 24 24" id="expand">
<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>
</symbol>

</defs>
</svg>

<div id="swagger-ui"></div>

<script>
(function () {
window.addEventListener("load", function () {
setTimeout(function () {

var logo = document.getElementsByClassName('link');

logo[0].children[0].alt = "My API";
logo[0].children[0].src = "/favicon-32x32.png";
});
}); })();
</script>

<!-- Workaround for https://github.com/swagger-api/swagger-editor/issues/1371 -->
<script>
if (window.navigator.userAgent.indexOf("Edge") > -1) {
console.log("Removing native Edge fetch in favor of swagger-ui's polyfill")
window.fetch = undefined;
}
</script>

<script src="./swagger-ui-bundle.js"> </script>
<script src="./swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function () {
var configObject = JSON.parse('<%= config_object.to_json %>');
var oauthConfigObject = JSON.parse('<%= oauth_config_object.to_json %>');

// Apply mandatory parameters
configObject.dom_id = "#swagger-ui";
configObject.presets = [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset];
configObject.layout = "StandaloneLayout";

// If oauth2RedirectUrl isn't specified, use the built-in default
if (!configObject.hasOwnProperty("oauth2RedirectUrl"))
configObject.oauth2RedirectUrl = window.location.href.replace("index.html", "oauth2-redirect.html");

// Build a system
const ui = SwaggerUIBundle(configObject);

// Apply OAuth config
ui.initOAuth(oauthConfigObject);
}
</script>
</body>

</html>
Loading

0 comments on commit 0a5fae1

Please sign in to comment.