diff --git a/app/assets/stylesheets/provider/_services.scss b/app/assets/stylesheets/provider/_services.scss deleted file mode 100644 index 345a382563..0000000000 --- a/app/assets/stylesheets/provider/_services.scss +++ /dev/null @@ -1,165 +0,0 @@ -@use 'provider/colors' as *; -@use 'provider/layouts/main' as *; -@use 'provider/typography' as *; -@use 'provider/legacy_theme'; -@use 'sass:math'; - -.button-to.action.new.service { - margin-bottom: line-height-times(-2); - font-weight: $font-weight-bold; - float: right; - padding-bottom: line-height-times(.125); - position: relative; - z-index: 100; -} - -@mixin overview-widget { - - .title { - @extend h2; - - margin-bottom: 0; - - a { - color: $font-color; - text-decoration: underline; - margin-bottom: line-height-times(.5); - } - - .button-to { - margin-top: line-height-times(.5); - } - } - - /* stylelint-disable-next-line no-descending-specificity -- FIXME */ - .button-to { - font-weight: $font-weight-bold; - } - - .listing { - clear: left; - list-style: none; - background-color: $white; - padding: line-height-times(1); - border: $border-width solid $border-color; - position: relative; - - &[data-hint] { - min-height: line-height-times(2); - } - - li { - border-bottom: $border-width solid $border-color; - margin-left: 0; - padding: line-height-times(.5) 0; - position: relative; - - .name { - font-weight: $font-weight-bold; - } - - &:first-child { - padding-top: 0; - } - - &:last-child { - border-bottom: none; - padding-bottom: 0; - } - - ul { - column-count: 2; - column-gap: line-height-times(3); - column-rule: $border-width solid $border-color; - list-style: none; - padding: line-height-times(math.div(2,3)) line-height-times(.5); - - /* stylelint-disable-next-line no-descending-specificity -- FIXME */ - li { - border-bottom: none; - display: inline-block; - padding-top: 0; - width: 100%; - } - } - } - } -} - -.overview-widget { - @include overview-widget; - - .left > h2, - .right > h2 { - margin: line-height-times(1) 0 0; - } -} - -.service-widget { - @extend .overview-widget; - - section { - margin-top: line-height-times(1); - - .flex-row { - align-items: center; - display: flex; - justify-content: space-between; - margin-top: line-height-times(2); - } - - /* stylelint-disable-next-line no-descending-specificity -- FIXME */ - h2 { - margin-top: line-height-times(2); - position: relative; - font-weight: $font-weight-normal; - - &:first-of-type { - margin-top: 0; - } - - & + .button-to { - float: right; - } - - a { - text-decoration: underline; - color: $link-hover-color; - - &:hover { - color: $link-color - } - } - } - - &[name='settings'] { - @extend .left-column; - } - - &[name='activity'] { - @extend .right-column; - } - } - - .latest-alerts, - .latest-apps, - .service-settings, - .service-plans, - .application-plans, - #mini-charts { - @extend .listing; - @include white-box-shadow; - - border: 0; - } - - p { - margin-top: 0; - min-width: 0; - } - - .outline-button.next { - float: none; - display: inline-block; - } -} diff --git a/app/assets/stylesheets/provider/_theme.scss b/app/assets/stylesheets/provider/_theme.scss index 1c20051dca..5d7e7bf9af 100644 --- a/app/assets/stylesheets/provider/_theme.scss +++ b/app/assets/stylesheets/provider/_theme.scss @@ -14,7 +14,6 @@ @forward 'provider/buttons'; @forward 'provider/links'; @forward 'provider/footer'; -@forward 'provider/services'; @forward 'provider/plans'; @forward 'provider/tables'; @forward 'provider/utilization'; diff --git a/app/assets/stylesheets/provider/admin/apiconfig/services/show.scss b/app/assets/stylesheets/provider/admin/apiconfig/services/show.scss new file mode 100644 index 0000000000..1ef301b76d --- /dev/null +++ b/app/assets/stylesheets/provider/admin/apiconfig/services/show.scss @@ -0,0 +1,10 @@ +.pf-c-card { + /* Custom class. PF4 don't offer md sized icons in pf-list */ + .pf-c-list.pf-m-icon-md { + --pf-c-list__item-icon--FontSize: var(--pf-c-card__body--FontSize); + } + + .pf-c-list li a:first-child { + font-weight: var(--pf-global--FontWeight--bold); + } +} diff --git a/app/controllers/api/services_controller.rb b/app/controllers/api/services_controller.rb index 9d18c1ccc0..2a8a04446a 100644 --- a/app/controllers/api/services_controller.rb +++ b/app/controllers/api/services_controller.rb @@ -14,6 +14,7 @@ class Api::ServicesController < Api::BaseController load_and_authorize_resource :service, through: :current_user, through_association: :accessible_services, except: [:create] + decorates_assigned :service helper_method :presenter def index @@ -25,9 +26,7 @@ def index end end - def show - @service = @service.decorate - end + def show; end def new activate_menu :products @@ -87,8 +86,6 @@ def destroy private - attr_reader :service - def integration_settings_updater_service ApiIntegration::SettingsUpdaterService.new(service: service, proxy: service.proxy) end diff --git a/app/decorators/alert_decorator.rb b/app/decorators/alert_decorator.rb new file mode 100644 index 0000000000..711e2f8320 --- /dev/null +++ b/app/decorators/alert_decorator.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class AlertDecorator < ApplicationDecorator + def icon + variant = case utilization_range + when 50 then :info + when 80, 90 then :warning + else :danger + end + h.pf_alert_icon variant, colored: true + end + + def link_to_app + if cinstance + h.link_to(cinstance.name, h.provider_admin_application_path(cinstance)) + else + h.tag.span '(deleted app)' + end + end + + def utilization_range + @utilization_range ||= h.utilization_range(level) + end +end diff --git a/app/decorators/plan_base_decorator.rb b/app/decorators/plan_base_decorator.rb index efc364be23..d676fc100b 100644 --- a/app/decorators/plan_base_decorator.rb +++ b/app/decorators/plan_base_decorator.rb @@ -25,4 +25,8 @@ def index_table_actions def contracts_path raise NoMethodError, "#{__method__} not implemented in #{self.class}" end + + def link_to_edit + raise NoMethodError, "#{__method__} not implemented in #{self.class}" + end end diff --git a/app/decorators/service_decorator.rb b/app/decorators/service_decorator.rb index 1b2a68efc6..d894eb7d0f 100644 --- a/app/decorators/service_decorator.rb +++ b/app/decorators/service_decorator.rb @@ -35,6 +35,10 @@ def published_application_plans ApplicationPlanDecorator.decorate_collection(application_plans.stock.published, context: { service: self }) end + def published_service_plans + ServicePlanDecorator.decorate_collection(service_plans.published) + end + def service_path if h.can?(:manage, :plans) h.admin_service_path(object) @@ -76,6 +80,58 @@ def as_json(options = {}) super.deep_transform_keys { |key| key.to_s.camelize(:lower).to_sym } end + def top_metrics + @top_metrics ||= metrics.top_level.limit(5) + end + + def refresh_service_discovery_link + url = h.service_discovery_usable? ? h.provider_admin_service_discovery_service_path(self) : h.service_discovery_presenter.authorize_url + + confirm = I18n.t('api.services.forms.definition_settings.refresh.confirmation', name: name) + label = I18n.t('api.services.forms.definition_settings.refresh.label') + h.action_link_to(:refresh, url, label:, + data: { confirm: }, + method: :put) + end + + def latest_alerts + @latest_alerts ||= account.alerts.by_service(self).latest.decorate + end + + def latest_applications + @latest_applications ||= cinstances.latest + end + + def traffic? + cinstances.where.not(first_traffic_at: nil).exists? + end + + def human_backend + { + "1" => "API key", + "2" => "App Id", + "oauth" => "OAuth", + "oidc" => "OpenID Connect" + }[service.proxy_authentication_method] + end + + # :reek:NilCheck + def friendly_service_settings + %i[buyers_manage_keys buyers_manage_apps buyer_plan_change_permission buyer_can_select_plan].map do |setting| + value = object.send(setting) + next if value.nil? + + subkey = case setting + when :buyer_plan_change_permission + value.to_sym + else + value ? :enabled : :disabled + end + + I18n.t("api.services.cards.settings.friendly_service_setting.#{setting}.#{subkey}").html_safe + end.compact + end + private def backend_api? diff --git a/app/decorators/service_plan_decorator.rb b/app/decorators/service_plan_decorator.rb index 1b7eb210c4..0c3a4fbf7a 100644 --- a/app/decorators/service_plan_decorator.rb +++ b/app/decorators/service_plan_decorator.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true class ServicePlanDecorator < PlanBaseDecorator + def link_to_edit + h.link_to(name, h.edit_admin_service_plan_path(self)) + end + + def total_contracts + I18n.t('api.services.cards.service_plans.contracts', count: contracts.size) + end + private def contracts_path diff --git a/app/helpers/api/services_helper.rb b/app/helpers/api/services_helper.rb index f9b85668c3..80fbcb3d31 100644 --- a/app/helpers/api/services_helper.rb +++ b/app/helpers/api/services_helper.rb @@ -4,56 +4,8 @@ def link_to_service service link_to service.name, admin_service_path(service) end - def list_items_or_empty collection, empty_message, &block - if collection.empty? - content_tag(:li, empty_message, :class => 'item empty') - else - collection.each do |item| - yield(item) - end - nil - end - end - - def friendly_service_setting service, setting - value = service.send setting - message, value = case setting - when :custom_keys_enabled - ['Custom application keys are VALUE', value ? 'enabled' : 'disabled'] - when :buyers_manage_keys - ['Users VALUE manage application keys', value ? 'can' : "cannot"] - when :buyer_can_select_plan - ['Users VALUE when creating an application', value ? 'can select a plan' : "cannot select a plan"] - when :buyer_plan_change_permission - value = case value.to_sym - when :request - "request plan change" - when :direct - "directly change plans" - when :none - "not change plans" - end - ['Users can VALUE', value] - when :buyers_manage_apps - ['Users VALUE manage applications', value ? 'can' : "can't"] - else - ["Setting #{setting} - VALUE", value] - end - - return unless message && value - - message.gsub('VALUE', content_tag(:strong, value)).html_safe - end - def delete_service_link(service, options = {}) msg = t('api.services.forms.definition_settings.delete_confirmation', name: j(service.name)) delete_link_for(admin_service_path(service), {data: { confirm: msg }, class: 'pf-c-button pf-m-danger', method: :delete}.merge(options) ) end - - def refresh_service_link(service, options = {}) - url = service_discovery_usable? ? provider_admin_service_discovery_service_path(service) : service_discovery_presenter.authorize_url - - msg = t('api.services.forms.definition_settings.refresh_confirmation', name: h(service.name)) - action_link_to(:refresh, url, {data: { confirm: msg }, method: :put}.merge(options) ) - end end diff --git a/app/helpers/buyers/cinstances_helper.rb b/app/helpers/buyers/cinstances_helper.rb index 182d8037b0..261eab7b84 100644 --- a/app/helpers/buyers/cinstances_helper.rb +++ b/app/helpers/buyers/cinstances_helper.rb @@ -1,12 +1,4 @@ module Buyers::CinstancesHelper - def link_to_cinstance_or_deleted(cinstance) - if cinstance - link_to(cinstance.name, provider_admin_application_path(cinstance)) - else - content_tag(:span, '(deleted app)', :class => 'deleted') - end - end - def link_to_plan_edit(plan) if can?(:manage, :plans) link_to(plan.name, edit_polymorphic_path([:admin, plan])) diff --git a/app/helpers/patternfly_components_helper.rb b/app/helpers/patternfly_components_helper.rb index 3190161b7d..852a0334a3 100644 --- a/app/helpers/patternfly_components_helper.rb +++ b/app/helpers/patternfly_components_helper.rb @@ -1,6 +1,16 @@ # frozen_string_literal: true module PatternflyComponentsHelper + # :reek:ControlParameter + def icon_color(variant) + case variant&.to_sym + when :danger then 'pf-u-danger-color-100' + when :info then 'pf-u-info-color-100' + when :success then 'pf-u-success-color-100' + when :warning then 'pf-u-warning-color-100' + else 'pf-u-default-color-200' + end + end def icon_name(variant) case variant&.to_sym @@ -14,10 +24,16 @@ def icon_name(variant) def icon_tag(variant) tag.div class: 'pf-c-alert__icon' do - tag.i class: "fas fa-fw fa-#{icon_name(variant)}", 'aria-hidden': 'true' + pf_alert_icon variant end end + # :reek:BooleanParameter, :reek:ControlParameter + def pf_alert_icon(variant, colored: false) + color_class = colored ? icon_color(variant) : '' + tag.i class: "fas fa-fw fa-#{icon_name(variant)} #{color_class}", 'aria-hidden': 'true' + end + def title_tag(title) tag.div class: 'pf-c-alert__title' do tag.p title diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index f2f2ce3fee..d4363d19bf 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -1,15 +1,6 @@ # frozen_string_literal: true module ServicesHelper - def human_backend(backend_version) - { - "1" => "API key", - "2" => "App Id", - "oauth" => "OAuth", - "oidc" => "OpenID Connect" - }[backend_version] - end - def plugin_language_name(service) service.deployment_option.remove('plugin_') end diff --git a/app/javascript/packs/account.scss b/app/javascript/packs/account.scss deleted file mode 100644 index ca7bfd22b8..0000000000 --- a/app/javascript/packs/account.scss +++ /dev/null @@ -1,2 +0,0 @@ -@import '~@patternfly/patternfly/components/Table/table.css'; -@import '~@patternfly/patternfly/layouts/Grid/grid.css'; diff --git a/app/javascript/packs/backends_used_list.ts b/app/javascript/packs/backends_used_list.ts index bb7e694831..7f6e013a54 100644 --- a/app/javascript/packs/backends_used_list.ts +++ b/app/javascript/packs/backends_used_list.ts @@ -15,6 +15,7 @@ document.addEventListener('DOMContentLoaded', () => { const { backends } = container.dataset BackendsUsedListCardWrapper({ - backends: safeFromJsonString(backends) ?? [] + backends: safeFromJsonString(backends) ?? [], + title: 'Backends used in this product' }, containerId) }) diff --git a/app/javascript/packs/dashboard.scss b/app/javascript/packs/dashboard.scss index 80bb2f4153..34553a90c4 100644 --- a/app/javascript/packs/dashboard.scss +++ b/app/javascript/packs/dashboard.scss @@ -2,7 +2,6 @@ @import '@patternfly/patternfly/components/Page/page.css'; @import '@patternfly/patternfly/components/Panel/panel.css'; @import '@patternfly/patternfly/components/Truncate/truncate.css'; -@import '@patternfly/patternfly/layouts/Grid/grid.css'; @import '@patternfly/patternfly/layouts/Level/level.css'; @import '@patternfly/patternfly/utilities/Spacing/spacing.css'; @import '@patternfly/patternfly/utilities/Text/text.css'; diff --git a/app/javascript/packs/provider_account_overview.scss b/app/javascript/packs/pf_datalist.scss similarity index 100% rename from app/javascript/packs/provider_account_overview.scss rename to app/javascript/packs/pf_datalist.scss diff --git a/app/javascript/packs/pf_grid.scss b/app/javascript/packs/pf_grid.scss new file mode 100644 index 0000000000..ea5626b2ee --- /dev/null +++ b/app/javascript/packs/pf_grid.scss @@ -0,0 +1 @@ +@import '@patternfly/patternfly/layouts/Grid/grid.css'; diff --git a/app/javascript/packs/pf_stack.scss b/app/javascript/packs/pf_stack.scss new file mode 100644 index 0000000000..1d4dc27b42 --- /dev/null +++ b/app/javascript/packs/pf_stack.scss @@ -0,0 +1 @@ +@import '@patternfly/patternfly/layouts/Stack/stack.css'; diff --git a/app/javascript/packs/pf_text.scss b/app/javascript/packs/pf_text.scss new file mode 100644 index 0000000000..3e356926cb --- /dev/null +++ b/app/javascript/packs/pf_text.scss @@ -0,0 +1 @@ +@import '@patternfly/patternfly/utilities/Text/text.css'; diff --git a/app/javascript/packs/products_used_list.ts b/app/javascript/packs/products_used_list.ts index 710083121b..bb39c98fd2 100644 --- a/app/javascript/packs/products_used_list.ts +++ b/app/javascript/packs/products_used_list.ts @@ -15,6 +15,7 @@ document.addEventListener('DOMContentLoaded', () => { const { products } = container.dataset ProductsUsedListCardWrapper({ + title: 'Products using this backend', products: safeFromJsonString(products) ?? [] }, containerId) }) diff --git a/app/javascript/src/BackendApis/components/ProductsUsedListCard.tsx b/app/javascript/src/BackendApis/components/ProductsUsedListCard.tsx index bedd40c64c..82c56e31e0 100644 --- a/app/javascript/src/BackendApis/components/ProductsUsedListCard.tsx +++ b/app/javascript/src/BackendApis/components/ProductsUsedListCard.tsx @@ -7,10 +7,11 @@ import { createReactWrapper } from 'utilities/createReactWrapper' import type { CompactListItem } from 'Common/components/CompactListCard' interface Props { + title: string; products: CompactListItem[]; } -const ProductsUsedListCard: React.FunctionComponent = ({ products }) => { +const ProductsUsedListCard: React.FunctionComponent = ({ title, products }) => { const [page, setPage] = useState(1) const [filteredProducts, setFilteredProducts] = useState(products) const searchInputRef = useRef(null) @@ -33,7 +34,7 @@ const ProductsUsedListCard: React.FunctionComponent = ({ products }) => { searchInputPlaceholder="Find a product" searchInputRef={searchInputRef} setPage={setPage} - tableAriaLabel="Products using this backend" + tableAriaLabel={title} onSearch={handleOnSearch} /> ) diff --git a/app/javascript/src/Common/components/CompactListCard.tsx b/app/javascript/src/Common/components/CompactListCard.tsx index cd6b40ab33..5977e2ec9f 100644 --- a/app/javascript/src/Common/components/CompactListCard.tsx +++ b/app/javascript/src/Common/components/CompactListCard.tsx @@ -3,6 +3,7 @@ import { ButtonVariant, Card, CardBody, + CardTitle, InputGroup, TextInput } from '@patternfly/react-core' @@ -24,6 +25,7 @@ interface Props { onSearch: (term?: string) => void; page: number; setPage: (page: number) => void; + title?: string; perPage?: number; searchInputPlaceholder?: string; tableAriaLabel?: string; @@ -38,6 +40,7 @@ const CompactListCard: React.FunctionComponent = ({ onSearch, page, setPage, + title, perPage = PER_PAGE, searchInputPlaceholder, tableAriaLabel @@ -68,6 +71,9 @@ const CompactListCard: React.FunctionComponent = ({ return ( + + {title} + = ({ backends }) => { +const BackendsUsedListCard: React.FunctionComponent = ({ backends, title }) => { const [page, setPage] = useState(1) const [filteredBackends, setFilteredBackends] = useState(backends) const searchInputRef = useRef(null) @@ -33,7 +34,8 @@ const BackendsUsedListCard: React.FunctionComponent = ({ backends }) => { searchInputPlaceholder="Find a backend" searchInputRef={searchInputRef} setPage={setPage} - tableAriaLabel="Backends used in this product" + tableAriaLabel={title} + title={title} onSearch={handleOnSearch} /> ) diff --git a/app/models/service.rb b/app/models/service.rb index 413e0597ac..d8db2b0c7c 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -251,14 +251,6 @@ def service_token active_service_token.try(:value) end - def latest_applications - cinstances.latest - end - - def has_traffic? - cinstances.where.not(first_traffic_at: nil).exists? - end - def proxiable? backend_version.is?(1, 2, :oauth) end diff --git a/app/views/api/services/cards/_analytics.html.slim b/app/views/api/services/cards/_analytics.html.slim new file mode 100644 index 0000000000..0b1ff8a2d8 --- /dev/null +++ b/app/views/api/services/cards/_analytics.html.slim @@ -0,0 +1,9 @@ +div class="pf-c-card" + div class="pf-c-card__header" + div class="pf-c-card__title" = t('.title') + div class="pf-c-card__body" + = render 'stats/inlinechart', metrics: service.top_metrics + div class="pf-c-card__footer" + = pf_link_to t('shared.view_more'), admin_service_stats_usage_path(service), + class: 'pf-m-inline', + title: t('.view_more') diff --git a/app/views/api/services/cards/_application_plans.html.slim b/app/views/api/services/cards/_application_plans.html.slim new file mode 100644 index 0000000000..dccfb18d36 --- /dev/null +++ b/app/views/api/services/cards/_application_plans.html.slim @@ -0,0 +1,20 @@ +- all_plans = service.application_plans.stock +- published_plans = service.published_application_plans + +div class="pf-c-card" + div class="pf-c-card__header" + div class="pf-c-card__title" = t('.title') + div class="pf-c-card__body" + - if all_plans.empty? + a class="pf-c-button pf-m-link pf-m-inline" type="button" href=new_admin_service_application_plan_path(service) + span class="pf-c-button__icon pf-m-start" + i class="fas fa-plus-circle" aria-hidden="true" + = t('.create_first_plan') + - else + ul class="pf-c-list pf-m-plain pf-m-bordered" + - published_plans.each do |plan| + li == "#{plan.link_to_edit} - #{plan.link_to_applications}" + - unless all_plans.empty? + div class="pf-c-card__footer" + = t('.footer_html', plans: service.link_to_application_plans, + apps: service.link_to_live_applications) diff --git a/app/views/api/services/cards/_backends_table.html.slim b/app/views/api/services/cards/_backends_table.html.slim index 79d0cc590d..b1100f8fa6 100644 --- a/app/views/api/services/cards/_backends_table.html.slim +++ b/app/views/api/services/cards/_backends_table.html.slim @@ -1,5 +1,4 @@ - content_for :javascripts = javascript_packs_with_chunks_tag 'backends_used_list' -h2 Backends used in this product div id='backends-used-list-container' data-backends=product.decorate.backends_table_data diff --git a/app/views/api/services/cards/_details.html.slim b/app/views/api/services/cards/_details.html.slim new file mode 100644 index 0000000000..d0e27077ad --- /dev/null +++ b/app/views/api/services/cards/_details.html.slim @@ -0,0 +1,35 @@ +div class="pf-c-card" + div class="pf-c-card__header" + div class="pf-c-card__title" = t('.title') + div class="pf-c-card__actions pf-m-no-offset" + a class="pf-c-button pf-m-link pf-m-inline" type="button" href=edit_admin_service_path(service) + span class="pf-c-button__icon pf-m-start" + i class="fas fa-pencil-alt" aria-hidden="true" + = t('.edit') + div class="pf-c-card__body" + dl class="pf-c-description-list pf-m-horizontal pf-m-2-col" + div class="pf-c-description-list__group" + dt class="pf-c-description-list__term" + span class="pf-c-description-list__text" Name + dd class="pf-c-description-list__description" + div class="pf-c-description-list__text" = service.name + div class="pf-c-description-list__group" + dt class="pf-c-description-list__term" + span class="pf-c-description-list__text" System name + dd class="pf-c-description-list__description" + div class="pf-c-description-list__text" = service.system_name + - if service.description.present? + div class="pf-c-description-list__group" + dt class="pf-c-description-list__term" + span class="pf-c-description-list__text" Description + dd class="pf-c-description-list__description" + div class="pf-c-description-list__text" = service.description + - if service_discovery_accessible? && service.discovered? + div class="pf-c-description-list__group" + dt class="pf-c-description-list__term" + span class="pf-c-description-list__text" Source + dd class="pf-c-description-list__description" + div class="pf-c-description-list__text" + div class="pf-l-flex pf-m-space-items-sm" + div class="pf-l-flex__item" OpenShift + div class="pf-l-flex__item" = service.refresh_service_discovery_link diff --git a/app/views/api/services/cards/_latest_alerts.html.slim b/app/views/api/services/cards/_latest_alerts.html.slim new file mode 100644 index 0000000000..b34a97e919 --- /dev/null +++ b/app/views/api/services/cards/_latest_alerts.html.slim @@ -0,0 +1,27 @@ +- alerts = service.latest_alerts + +div class="pf-c-card" + div class="pf-c-card__header" + div class="pf-c-card__title" = t('.title') + div class="pf-c-card__body" + ul class="pf-c-list pf-m-plain pf-m-bordered pf-m-icon-md" + - if alerts.empty? + div class="pf-l-flex pf-m-space-items-sm" + div class="pf-l-flex__item" + = pf_alert_icon :success, colored: true + div class="pf-l-flex__item" + span = t('.no_alerts') + - else + - alerts.each do |alert| + li class="pf-c-list__item" + span class="pf-c-list__item-icon" + = alert.icon + span class="pf-c-list__item-text utilization" + p + b == "#{alert.message} (#{colorize_utilization(alert.level)})" + == "#{alert.link_to_app} at #{l alert.timestamp}" + - unless alerts.empty? + div class="pf-c-card__footer" + = pf_link_to t('shared.view_more'), admin_service_alerts_path(service), + class: 'pf-m-inline', + title: t('.view_more') diff --git a/app/views/api/services/cards/_latest_apps.html.slim b/app/views/api/services/cards/_latest_apps.html.slim new file mode 100644 index 0000000000..d2c43cee82 --- /dev/null +++ b/app/views/api/services/cards/_latest_apps.html.slim @@ -0,0 +1,21 @@ +- apps = service.latest_applications + +div class="pf-c-card" + div class="pf-c-card__header" + div class="pf-c-card__title" = t('.title') + div class="pf-c-card__body" + ul class="pf-c-list pf-m-plain pf-m-bordered" + - if apps.empty? + a class="pf-c-button pf-m-link pf-m-inline" href=new_admin_service_application_path(service) + span class="pf-c-button__icon pf-m-start" + i class="fas fa-plus-circle" aria-hidden="true" + = t('.create') + - else + - apps.each do |app| + - account = app.user_account + li == "#{link_to app.name, provider_admin_application_path(app)} from #{link_to account.org_name, admin_buyers_account_path(account)}" + - unless apps.empty? + div class="pf-c-card__footer" + = pf_link_to t('shared.view_more'), admin_service_applications_path(service), + class: 'pf-m-inline', + title: t('.view_more') diff --git a/app/views/api/services/cards/_service_plans.html.slim b/app/views/api/services/cards/_service_plans.html.slim new file mode 100644 index 0000000000..717dc9c6ba --- /dev/null +++ b/app/views/api/services/cards/_service_plans.html.slim @@ -0,0 +1,21 @@ +- all_plans = service.service_plans +- published_plans = service.published_service_plans + +div class="pf-c-card" + div class="pf-c-card__header" + div class="pf-c-card__title" = t('.title') + div class="pf-c-card__body" + - if all_plans.empty? + a class="pf-c-button pf-m-link pf-m-inline" type="button" href=new_admin_service_service_plan_path(service) + span class="pf-c-button__icon pf-m-start" + i class="fas fa-plus-circle" aria-hidden="true" + = t('.create_first_plan') + - else + ul class="pf-c-list pf-m-plain pf-m-bordered" + - published_plans.each do |plan| + li == "#{plan.link_to_edit} - #{plan.total_contracts}" + - unless all_plans.empty? + div class="pf-c-card__footer" + = t('.footer_html', plans: link_to(pluralize(all_plans.size, 'service plan'), admin_service_service_plans_path(service)), + published: published_plans.size, + contracts: t('.contracts', count: service.contracts.service.size)) diff --git a/app/views/api/services/cards/_settings.html.slim b/app/views/api/services/cards/_settings.html.slim new file mode 100644 index 0000000000..a099608f50 --- /dev/null +++ b/app/views/api/services/cards/_settings.html.slim @@ -0,0 +1,16 @@ +div class="pf-c-card" + div class="pf-c-card__header" + div class="pf-c-card__title" = t('.title') + div class="pf-c-card__body" + ul class="pf-c-list pf-m-plain pf-m-bordered" + - if service.deployment_option.present? && service.traffic? + li == "Integrated through #{t(service.deployment_option, scope: "deployment_options.phrased")}" + li == "Authenticated by #{service.human_backend}" + li == "ID for API calls is #{service.id} and system name is #{service.system_name}" + - service.friendly_service_settings.each do |setting| + li = setting + + div class="pf-c-card__footer" + = t('.view_more_html', integration_link: link_to('configuration', admin_service_integration_path(service), title: t('.configuration')), + metrics_link: link_to('methods', admin_service_metrics_path(service), title: t('.methods')), + settings_link: link_to('settings', settings_admin_service_path(service), title: t('.settings'))) diff --git a/app/views/api/services/show.html.slim b/app/views/api/services/show.html.slim index 341306ab48..19d9a5d41b 100644 --- a/app/views/api/services/show.html.slim +++ b/app/views/api/services/show.html.slim @@ -1,138 +1,48 @@ -- content_for :page_header_title, 'Product Overview' +- content_for :page_header_title, t('.page_header_title', name: service.name) - content_for :page_header_annotation do - = render partial: 'shared/annotations', locals: { resource: @service, plain: true } - -section.Section - .SettingsBox - = pf_link_to 'edit', edit_admin_service_path(@service), class: "SettingsBox-toggle" - dl.SettingsBox-summary.u-dl data-state="open" - dt.u-dl-term Name - dd.u-dl-definition = @service.name - dt.u-dl-term System Name - dd.u-dl-definition = @service.system_name - - if @service.description.present? - dt.u-dl-term Description - dd.u-dl-definition = @service.description - - if service_discovery_accessible? && @service.discovered? - dt.u-dl-term Source - dd.u-dl-definition - 'OpenShift - = refresh_service_link(@service, label: 'refresh') - -- service = @service - -section class="service-widget" - div.content-service - section[name="settings"] - - - if can? :manage, :partners - - - if can?(:show, Cinstance) - h2 = link_to 'Latest Apps', admin_service_applications_path(service), :title => 'Show all applications for this service' - ol.latest-apps - = list_items_or_empty service.latest_applications, 'There are no latest applications.' do |cinstance| - - if cinstance.user_account.present? - li.item - => link_to cinstance.name, provider_admin_application_path(cinstance) - ' from - = link_to cinstance.user_account.org_name, admin_buyers_account_path(cinstance.user_account) - - - h2 = link_to 'Latest alerts', admin_service_alerts_path(service), :title => 'Show all limit alerts for this service' - ol.latest-alerts - = list_items_or_empty current_account.alerts.by_service(service).latest, 'There are no alerts.' do |alert| - li.item.utilization - = link_to_cinstance_or_deleted(alert.cinstance) - | at - = l alert.timestamp - | ( - = colorize_utilization(alert.level) - | ) - br - span.name - = alert.message - - - if can? :manage, :plans - .flex-row - h2.flex-item - ' Published - => link_to 'Application Plans', admin_service_application_plans_path(service) - = action_link_to :new, new_admin_service_application_plan_path(service), - label: 'Create application plan', class: 'flex-item' - - ul.application-plans[data-hint="published"] - = list_items_or_empty service.published_application_plans, 'There are no published application plans. Create one!' do |plan| - li.plan.item - => plan.link_to_edit(class: :name) - ' - - = plan.link_to_applications - p - ' You have - = service.link_to_application_plans - ' with a total of - = service.link_to_live_applications - | . - - - if (can? :manage, :service_plans) && (current_account.settings.service_plans_ui_visible?) - .flex-row - h2.flex-item - ' Published - => link_to 'Service plans', admin_service_service_plans_path(service) - = action_link_to :new, new_admin_service_service_plan_path(service), - label: 'Create Service Plan', class: 'flex-item' - - ul.service-plans[data-hint="published"] - = list_items_or_empty service.service_plans.published, 'There are no published service plans. Create one!' do |plan| - li.plan.item - => link_to plan.name, edit_admin_service_plan_path(plan), :class => :name - ' - - = pluralize plan.contracts.size, 'contract' - p - ' You have - = link_to pluralize(service.service_plans.size, 'service plan'),admin_service_service_plans_path(service) - | ( - = service.service_plans.published.size - ' published) with a total of - = pluralize service.contracts.service.size, 'contract' - | . - - - if can? :manage, :partners - h2 - = link_to 'Configuration', admin_service_integration_path(service), title: 'Change integration settings for this service' - ' , - => link_to 'Methods', admin_service_metrics_path(service) - ' and - =< link_to 'Settings', settings_admin_service_path(service), :title => "Edit service settings" - - ul.service-settings[data-hint="basics"] - - if service.deployment_option.present? && service.has_traffic? - li.item - ' Integrated through - strong => t(service.deployment_option, scope: "deployment_options.phrased") - - li.item - ' Authenticated by - strong - => human_backend service.proxy_authentication_method - li.item - ' ID for API calls is - strong - => service.id - ' and system name is - strong - = service.system_name - - - [:buyers_manage_keys, :buyers_manage_apps, :buyer_plan_change_permission, :buyer_can_select_plan ].map { |setting| friendly_service_setting(service, setting) }.compact.each do |setting| - li.item - = setting - - - if current_user.accessible_services.empty? - = render 'shared/service_access' - - section[name="activity"] - - - if can? :manage, :monitoring - h2 = link_to 'Analytics', admin_service_stats_usage_path(service), title: 'Show more stats for this service' - = render 'stats/inlinechart', metrics: service.metrics.top_level - - = render 'api/services/cards/backends_table', product: service + = render partial: 'shared/annotations', locals: { resource: service, plain: true } + +- content_for :javascripts do + = stylesheet_link_tag 'provider/admin/apiconfig/services/show' + = stylesheet_packs_chunks_tag 'pf_grid', 'pf_description_list', 'pf_text', 'pf_stack' + +div class="pf-l-grid pf-m-gutter" + div class="pf-l-grid__item pf-m-12-col" + = render 'api/services/cards/details', service: + + / Left column + div class="pf-l-grid__item pf-m-6-col" + div class="pf-l-grid__item" + div class="pf-l-flex pf-m-column pf-m-flex-1" + - if can?(:manage, :partners) + - if can?(:show, Cinstance) + div class="pf-l-flex__item" + = render 'api/services/cards/latest_apps', service: + + div class="pf-l-flex__item" + = render 'api/services/cards/latest_alerts', service: + + - if can?(:manage, :plans) + div class="pf-l-flex__item" + = render 'api/services/cards/application_plans', service: + + - if can?(:manage, :service_plans) && current_account.settings.service_plans_ui_visible? + div class="pf-l-flex__item" + = render 'api/services/cards/service_plans', service: + + - if can?(:manage, :partners) + div class="pf-l-flex__item" + = render 'api/services/cards/settings', service: + + - if current_user.accessible_services.empty? + = render 'shared/service_access' + + / Right column + div class="pf-l-grid__item pf-m-6-col" + div class="pf-l-flex pf-m-column pf-m-flex-1" + - if can?(:manage, :monitoring) + div class="pf-l-flex__item" + = render 'api/services/cards/analytics', service: + + div class="pf-l-flex__item" + = render 'api/services/cards/backends_table', product: service diff --git a/app/views/buyers/accounts/show.html.slim b/app/views/buyers/accounts/show.html.slim index af445a4820..a7b4a233a1 100644 --- a/app/views/buyers/accounts/show.html.slim +++ b/app/views/buyers/accounts/show.html.slim @@ -1,5 +1,5 @@ - content_for :javascripts do - = stylesheet_packs_chunks_tag 'account', 'pf_form' + = stylesheet_packs_chunks_tag 'pf_grid', 'pf_table', 'pf_form' - content_for :menu do = render 'menu' diff --git a/app/views/provider/admin/accounts/show.html.slim b/app/views/provider/admin/accounts/show.html.slim index 901525a6ff..a7bee0685c 100644 --- a/app/views/provider/admin/accounts/show.html.slim +++ b/app/views/provider/admin/accounts/show.html.slim @@ -1,7 +1,7 @@ - content_for :page_header_title, 'Overview' - content_for :javascripts do - = javascript_packs_with_chunks_tag 'provider_account_overview' + = stylesheet_packs_chunks_tag 'pf_datalist' div id="key-overview" h2 Provider API key diff --git a/app/views/provider/admin/dashboards/show.html.slim b/app/views/provider/admin/dashboards/show.html.slim index 7f27f7d04e..78442dea71 100644 --- a/app/views/provider/admin/dashboards/show.html.slim +++ b/app/views/provider/admin/dashboards/show.html.slim @@ -3,6 +3,7 @@ = stylesheet_link_tag 'provider/admin/dashboard' - content_for :javascripts do + = stylesheet_packs_chunks_tag 'pf_grid' = javascript_packs_with_chunks_tag 'dashboard' - if Features::QuickstartsConfig.enabled? diff --git a/config/locales/en.yml b/config/locales/en.yml index a0326782ac..263c7998a1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1475,9 +1475,64 @@ en: forms: definition_settings: delete_confirmation: Are you sure you want to delete the service '%{name}'? - refresh_confirmation: - This action will overwrite the API's Private URL and automatically created ActiveDocs. - Are you sure you want to proceed? + refresh: + confirmation: + This action will overwrite the API's Private URL and automatically created ActiveDocs. + Are you sure you want to proceed? + label: Refresh + cards: + analytics: + title: Analytics + view_more: View more stats for this service + application_plans: + title: Published Application Plans + footer_html: 'You have %{plans} with a total of %{apps}.' + create_first_plan: Create your first application plan + details: + title: Details + edit: Edit + latest_alerts: + title: Latest Alerts + no_alerts: There are no alerts + view_more: View all limit alerts for this service + latest_apps: + create: Create an application + title: Latest Apps + view_more: View all applications for this service + service_plans: + title: Published Service Plans + footer_html: You have %{plans} (%{published} published) with a total of %{contracts}. + create_first_plan: Create your first service plan + contracts: + one: '%{count} subscription' + other: '%{count} subscriptions' + settings: + configuration: Edit integration settings for this service + methods: Manage metrics and method for this service + settings: Edit settings for this service + title: Configuration and Settings + friendly_service_setting: + buyers_manage_keys: + enabled: "Users can manage application keys" + disabled: "Users cannot manage application keys" + buyer_can_select_plan: + enabled: "Users can select a plan when creating an application" + disabled: "Users cannot select a plan when creating an application" + buyer_plan_change_permission: + request: "Users can request plan change" + direct: "Users can directly change plans" + none: "Users can not change plans" + buyers_manage_apps: + enabled: "Users can manage applications" + disabled: "Users can't manage applications" + view_more_html: View all %{integration_link}, %{metrics_link} and %{settings_link} options. + show: + create_service_plan: Create service plan + page_header_title: Product %{name} + support_email: + remove_success: Custom support email removed from product + update_success: Support email updated successfully + error: Couldn't update support email update: success: Product information updated error: Couldn't update Product @@ -1485,10 +1540,6 @@ en: plans_info: When 'Request plan change' is selected you will be informed about users' requests by email - you can then execute it in administration interface. - support_email: - remove_success: Custom support email removed from product - update_success: Support email updated successfully - error: Couldn't update support email supports: update: success: Support information was updated @@ -2457,3 +2508,4 @@ en: nav: earnings: Earnings by month plugins_deprecation: This product currently uses a plugin as the deployment option. Your integration will keep on working but we no longer support plugins. + view_more: View more diff --git a/features/api/services/alerts.feature b/features/api/services/alerts.feature index 0b4e284f79..9c67ac24ea 100644 --- a/features/api/services/alerts.feature +++ b/features/api/services/alerts.feature @@ -31,7 +31,7 @@ Feature: Product > Analytics > Alerts Scenario: Navigation via product overview Given the current page is the provider dashboard When follow "My Product" within the apis dashboard widget - And follow "Show all limit alerts for this service" + And follow "View all limit alerts for this service" Then the current page is the alerts of "My Product" Scenario: Listing alerts diff --git a/features/old/stats/provider_side.feature b/features/old/stats/provider_side.feature index 620dd5be5d..e8b9ae6c40 100644 --- a/features/old/stats/provider_side.feature +++ b/features/old/stats/provider_side.feature @@ -13,19 +13,19 @@ Feature: Provider stats Scenario: Stats access And I follow "API" - And I follow "Analytics" + And I follow "View more stats for this service" And I follow "Traffic" Then I should be on the provider stats usage page Scenario: Usage stats And I follow "API" - And I follow "Analytics" + And I follow "View more stats for this service" Then I should see "Traffic" Scenario: Top applications (multiple applications mode) Given a buyer "bob" signed up to provider "foo.3scale.localhost" And I follow "API" - And I follow "Analytics" + And I follow "View more stats for this service" And I go to the provider stats apps page Then I should see "Top Applications" in a header And I should see a chart called "chart" @@ -37,7 +37,7 @@ Feature: Provider stats | API | Default | true | And a buyer "bob" signed up to application plan "Default" And I follow "API" - And I follow "Analytics" + And I follow "View more stats for this service" And I follow "Top Applications" Then I should see "Top Applications" in a header @@ -47,7 +47,7 @@ Feature: Provider stats And a backend And the backend is used by the product When I go to the overview page of product "My API" - And I follow "Analytics" + And I follow "View more stats for this service" And I follow "Traffic" Then I should see "Hits (hits)" @@ -57,7 +57,7 @@ Feature: Provider stats And a service "My API" of the provider And the backend is used by the product When I go to the overview page of product "My API" - And I follow "Analytics" + And I follow "View more stats for this service" And I follow "Traffic" Then I should see "Hits (hits)" @@ -75,7 +75,7 @@ Feature: Provider stats | fiona | June 18th 2009 | When I log in as provider "foo.3scale.localhost" And I follow "API" - And I follow "Analytics" + And I follow "View more stats for this service" And I follow "Signups" Then I should see these buyers in the latest signups table: | fiona | @@ -102,7 +102,7 @@ Feature: Provider stats | FancyWidget | Oct 18th 2010 | When I log in as provider "foo.3scale.localhost" And I follow "API" - And I follow "Analytics" + And I follow "View more stats for this service" And I follow "Signups" Then I should see these applications in the latest signups table: | FancyWidget | diff --git a/test/decorators/alert_decorator_test.rb b/test/decorators/alert_decorator_test.rb new file mode 100644 index 0000000000..5da89fcf73 --- /dev/null +++ b/test/decorators/alert_decorator_test.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'test_helper' + +class AlertDecoratorTest < Draper::TestCase + + test 'icon for each alert level' do + expected_icons = { + 50 => 'fa-info-circle', + 80 => 'fa-exclamation-triangle', + 90 => 'fa-exclamation-triangle', + 100 => 'fa-exclamation-circle', + 120 => 'fa-exclamation-circle', + 150 => 'fa-exclamation-circle', + 200 => 'fa-exclamation-circle', + 300 => 'fa-exclamation-circle' + } + expected_icons.each do |level, icon| + alert = FactoryBot.build(:alert, level: level) + helpers.stubs(:utilization_range).returns(level) + assert_match icon, AlertDecorator.decorate(alert).icon + end + end + + test 'link_to_app without cinstance' do + alert = FactoryBot.build(:alert, cinstance: nil, account: FactoryBot.build(:account)) + assert_match '(deleted app)', AlertDecorator.decorate(alert).link_to_app + end + + test 'link_to_app with cinstance' do + alert = FactoryBot.build(:alert) + helpers.stubs(:provider_admin_application_path).with(alert.cinstance).returns('/path/to/app') + + result = AlertDecorator.decorate(alert).link_to_app + assert_match alert.cinstance.name, result + assert_match '/path/to/app', result + end +end diff --git a/test/decorators/service_decorator_test.rb b/test/decorators/service_decorator_test.rb index 924e8caf0d..f306fdaab6 100644 --- a/test/decorators/service_decorator_test.rb +++ b/test/decorators/service_decorator_test.rb @@ -59,6 +59,19 @@ def test_product_link assert_nil decorator.link end + # :reek:DuplicateMethodCall + def test_traffic? + service = FactoryBot.create(:simple_service) + plan = FactoryBot.create(:simple_application_plan, issuer: service) + app = FactoryBot.create(:simple_cinstance, plan: plan) + decorator = ServiceDecorator.decorate(service) + + refute decorator.traffic? + + app.update(first_traffic_at: Time.zone.now) + assert decorator.traffic? + end + private def stub_can(action, object) diff --git a/test/factories/alert.rb b/test/factories/alert.rb new file mode 100644 index 0000000000..592fd99b7d --- /dev/null +++ b/test/factories/alert.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory(:alert) do + association :cinstance + account { cinstance.user_account } + sequence(:alert_id) { |n| "alert-#{n}" } + level { 50 } + utilization { 0.5 } + timestamp { Time.zone.now } + end +end diff --git a/test/factories/other.rb b/test/factories/other.rb index a4e04d59de..9355bfbaf4 100644 --- a/test/factories/other.rb +++ b/test/factories/other.rb @@ -56,12 +56,13 @@ factory(:metric) do association :owner, factory: :service - sequence(:friendly_name) { |n| "Metric #{n}" } - sequence(:unit) { |m| "metric_#{m}" } + friendly_name { "Metric #{SecureRandom.hex(4)}" } + unit { 'hit' } # TODO: use this factory throughout the codebase factory(:method) do parent { owner.metrics.hits } + friendly_name { "Method #{SecureRandom.hex(4)}" } end end diff --git a/test/unit/service_test.rb b/test/unit/service_test.rb index 00c3a7299b..0f714828b0 100644 --- a/test/unit/service_test.rb +++ b/test/unit/service_test.rb @@ -185,22 +185,6 @@ class ServiceTest < ActiveSupport::TestCase assert_contains service.cinstances, cinstance end - test '#has_traffic?' do - service = FactoryBot.create(:simple_service) - plan = FactoryBot.create(:application_plan, issuer: service) - - buyer1 = FactoryBot.create(:simple_buyer) - buyer2 = FactoryBot.create(:simple_buyer) - - app1 = buyer1.buy!(plan) - buyer2.buy!(plan) - - assert_equal false, service.has_traffic? - - app1.update(first_traffic_at: Time.zone.now) - assert_equal true, service.has_traffic? - end - test '#mode_type' do assert_nil Service.new.mode_type