Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
71.21% covered (warning)
71.21%
47 / 66
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
WP_Dashboard_Odyssey_Widget
71.21% covered (warning)
71.21%
47 / 66
66.67% covered (warning)
66.67%
4 / 6
18.68
0.00% covered (danger)
0.00%
0 / 1
 register_widget
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 render
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
 render_loading_skeleton
35.71% covered (danger)
35.71%
10 / 28
0.00% covered (danger)
0.00%
0 / 1
5.39
 load_admin_scripts
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 maybe_load_admin_scripts
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 is_widget_hidden
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * A class that adds Jetpack Stats widget for WP Dashboard.
4 *
5 * @package automattic/jetpack-stats-admin
6 */
7
8namespace Automattic\Jetpack\Stats_Admin;
9
10use Automattic\Jetpack\Modules;
11use Automattic\Jetpack\Status\Host;
12
13/**
14 * Responsible for loading Jetpack Stats widget for WP Dashboard.
15 *
16 * @package jetpack-stats-admin
17 */
18class WP_Dashboard_Odyssey_Widget {
19    const DASHBOARD_WIDGET_ID = 'jetpack_summary_widget';
20
21    /**
22     * Add the widget to the WordPress dashboard of a connected site with Stats active.
23     *
24     * Hook it to `wp_dashboard_setup`.
25     *
26     * @return bool Whether the widget was added.
27     */
28    public static function register_widget() {
29        /**
30         * Filter whether the Jetpack Stats dashboard widget should be shown to the current user.
31         * By default, the dashboard widget is shown to users who can manage options or view Stats.
32         *
33         * @module stats
34         * @since-jetpack 11.9
35         *
36         * @param bool Whether to show the widget to the current user.
37         */
38        if ( ! apply_filters( 'jetpack_stats_dashboard_widget_show_to_user', current_user_can( 'manage_options' ) || current_user_can( 'view_stats' ) ) ) {
39            return false;
40        }
41
42        if ( ! Main::is_site_connected() || ! ( new Modules() )->is_active( 'stats' ) ) {
43            return false;
44        }
45
46        $widget = new self();
47        wp_add_dashboard_widget(
48            self::DASHBOARD_WIDGET_ID,
49            /** "Stats" is a product name, do not translate. */
50            'Jetpack Stats',
51            array( $widget, 'render' )
52        );
53        $widget->maybe_load_admin_scripts();
54
55        return true;
56    }
57
58    /**
59     * Renders the widget and fires a dashboard widget action.
60     */
61    public function render() {
62        // The widget is always rendered, so if it was hidden and then toggled open, we need to ask user to refresh the page to load data properly.
63        $is_toggled_open = $this->is_widget_hidden();
64        ?>
65        <div id="dashboard_stats" class="jp-stats-widget">
66            <div class="hide-if-js"><?php esc_html_e( 'Your Jetpack Stats widget requires JavaScript to function properly.', 'jetpack-stats-admin' ); ?></div>
67            <div class="hide-if-no-js">
68                <?php if ( $is_toggled_open ) : ?>
69                    <p><?php esc_html_e( 'Please reload the page to see your stats...', 'jetpack-stats-admin' ); ?></p>
70                <?php else : ?>
71                    <?php $this->render_loading_skeleton(); ?>
72                <?php endif; ?>
73            </div>
74        </div>
75        <?php
76        /**
77         * Fires when the dashboard is loaded, but no longer used anywhere in the Jetpack plugin.
78         * The action is still available for backward compatibility.
79         *
80         * @since 3.4.0
81         */
82        do_action( 'jetpack_dashboard_widget' );
83    }
84
85    /**
86     * Renders the widget's skeleton for the time before its script runs; the loader then swaps in
87     * its own copy. Kept in step with wp-calypso's apps/odyssey-stats/src/widget/loading-skeleton.ts.
88     */
89    private function render_loading_skeleton() {
90        // The gates the widget applies to its Site protection card: not on Simple sites, nor on
91        // a standalone Stats plugin without Jetpack. See Odyssey_Config_Data::get_config_data().
92        $has_site_protection = ! ( new Host() )->is_wpcom_simple() && defined( 'JETPACK__VERSION' );
93        ?>
94        <div class="jp-stats-widget-skeleton" role="status" aria-label="<?php esc_attr_e( 'Loading', 'jetpack-stats-admin' ); ?>">
95            <style>
96                .jp-stats-widget-skeleton{container-type:inline-size;margin:-11px -12px -12px;border-radius:0 0 8px 8px;background:#f6f7f7;font-size:0}
97                .jp-stats-widget-skeleton__inner{display:flex;flex-direction:column;gap:8px;padding:8px}
98                .jp-stats-widget-skeleton__card{border-radius:7px;box-shadow:0 0 0 1px rgba(0,0,0,.1);background:#fff}
99                .jp-stats-widget-skeleton__header{display:flex;justify-content:space-between;align-items:center;min-block-size:32px;padding:8px}
100                .jp-stats-widget-skeleton__body{padding:8px 12px 12px}
101                .jp-stats-widget-skeleton__body.is-tabbed{padding-block-start:0}
102                .jp-stats-widget-skeleton__metrics{display:flex;gap:16px}
103                .jp-stats-widget-skeleton__metric{display:flex;flex-direction:column;flex:1 1 0}
104                .jp-stats-widget-skeleton__tabs{display:flex;margin-inline:-12px;border-block-end:1px solid #dbdbdb}
105                .jp-stats-widget-skeleton__tab{flex:1;display:flex;justify-content:center;padding-block:18px}
106                .jp-stats-widget-skeleton__rows{display:flex;flex-direction:column;margin-block-start:8px}
107                .jp-stats-widget-skeleton__row{display:flex;justify-content:space-between;align-items:center;gap:16px;min-block-size:32px}
108                .jp-stats-widget-skeleton__link{display:flex;align-items:center;block-size:18.2px;margin-block-start:16px}
109                .jp-stats-widget-skeleton__link.is-view-all{margin-block-start:8px}
110                .jp-stats-widget-skeleton__footer{display:flex;justify-content:space-between;align-items:center;padding:8px}
111                .jp-stats-widget-skeleton__bar{display:block;block-size:12px;border-radius:4px;background:#f4f4f4}
112                .jp-stats-widget-skeleton__bar.is-title{inline-size:140px;block-size:16px}
113                .jp-stats-widget-skeleton__bar.is-control{inline-size:112px;block-size:32px}
114                .jp-stats-widget-skeleton__bar.is-label{inline-size:72px;block-size:14px;margin-block:3px}
115                .jp-stats-widget-skeleton__bar.is-value{inline-size:64px;block-size:28px;margin-block:8px 4px}
116                .jp-stats-widget-skeleton__bar.is-chart{block-size:160px;border-radius:8px;margin-block-start:16px}
117                .jp-stats-widget-skeleton__bar.is-tab{inline-size:96px}
118                .jp-stats-widget-skeleton__bar.is-row-title{inline-size:60%}
119                .jp-stats-widget-skeleton__bar.is-row-value{inline-size:46px}
120                .jp-stats-widget-skeleton__bar.is-link{inline-size:72px}
121                .jp-stats-widget-skeleton__bar.is-logo{inline-size:88px;block-size:20px}
122                .jp-stats-widget-skeleton__bar.is-footer-link{inline-size:80px}
123                @container (max-inline-size:360px){.jp-stats-widget-skeleton__metrics{flex-direction:column}.jp-stats-widget-skeleton__metric{flex:none}}
124                @media not (prefers-reduced-motion){.jp-stats-widget-skeleton__bar{animation:jp-stats-widget-skeleton-pulse 1.5s ease-in-out infinite}}
125                @media (forced-colors:active){.jp-stats-widget-skeleton__bar{outline:1px solid CanvasText;outline-offset:-1px}}
126                @keyframes jp-stats-widget-skeleton-pulse{0%,100%{opacity:1}50%{opacity:.4}}
127            </style>
128            <div class="jp-stats-widget-skeleton__inner">
129                <div class="jp-stats-widget-skeleton__card">
130                    <div class="jp-stats-widget-skeleton__header">
131                        <span class="jp-stats-widget-skeleton__bar is-title"></span>
132                        <span class="jp-stats-widget-skeleton__bar is-control"></span>
133                    </div>
134                    <div class="jp-stats-widget-skeleton__body">
135                        <div class="jp-stats-widget-skeleton__metrics">
136                            <div class="jp-stats-widget-skeleton__metric">
137                                <span class="jp-stats-widget-skeleton__bar is-label"></span>
138                                <span class="jp-stats-widget-skeleton__bar is-value"></span>
139                            </div>
140                            <div class="jp-stats-widget-skeleton__metric">
141                                <span class="jp-stats-widget-skeleton__bar is-label"></span>
142                                <span class="jp-stats-widget-skeleton__bar is-value"></span>
143                            </div>
144                        </div>
145                        <span class="jp-stats-widget-skeleton__bar is-chart"></span>
146                        <div class="jp-stats-widget-skeleton__link"><span class="jp-stats-widget-skeleton__bar is-link"></span></div>
147                    </div>
148                </div>
149                <div class="jp-stats-widget-skeleton__card">
150                    <div class="jp-stats-widget-skeleton__header">
151                        <span class="jp-stats-widget-skeleton__bar is-title"></span>
152                    </div>
153                    <div class="jp-stats-widget-skeleton__body is-tabbed">
154                        <div class="jp-stats-widget-skeleton__tabs">
155                            <div class="jp-stats-widget-skeleton__tab"><span class="jp-stats-widget-skeleton__bar is-tab"></span></div>
156                            <div class="jp-stats-widget-skeleton__tab"><span class="jp-stats-widget-skeleton__bar is-tab"></span></div>
157                        </div>
158                        <div class="jp-stats-widget-skeleton__rows">
159                            <div class="jp-stats-widget-skeleton__row">
160                                <span class="jp-stats-widget-skeleton__bar is-row-title"></span>
161                                <span class="jp-stats-widget-skeleton__bar is-row-value"></span>
162                            </div>
163                        </div>
164                        <div class="jp-stats-widget-skeleton__link is-view-all"><span class="jp-stats-widget-skeleton__bar is-link"></span></div>
165                    </div>
166                </div>
167                <?php if ( $has_site_protection ) : ?>
168                <div class="jp-stats-widget-skeleton__card">
169                    <div class="jp-stats-widget-skeleton__header">
170                        <span class="jp-stats-widget-skeleton__bar is-title"></span>
171                    </div>
172                    <div class="jp-stats-widget-skeleton__body">
173                        <div class="jp-stats-widget-skeleton__metrics">
174                            <div class="jp-stats-widget-skeleton__metric">
175                                <span class="jp-stats-widget-skeleton__bar is-label"></span>
176                                <span class="jp-stats-widget-skeleton__bar is-value"></span>
177                            </div>
178                            <div class="jp-stats-widget-skeleton__metric">
179                                <span class="jp-stats-widget-skeleton__bar is-label"></span>
180                                <span class="jp-stats-widget-skeleton__bar is-value"></span>
181                            </div>
182                        </div>
183                    </div>
184                </div>
185                <?php endif; ?>
186                <div class="jp-stats-widget-skeleton__footer">
187                    <span class="jp-stats-widget-skeleton__bar is-logo"></span>
188                    <span class="jp-stats-widget-skeleton__bar is-footer-link"></span>
189                </div>
190            </div>
191        </div>
192        <?php
193    }
194
195    /**
196     * Load the admin scripts.
197     */
198    public function load_admin_scripts() {
199        $config_data = ( new Odyssey_Config_Data() )->get_data();
200        ( new Odyssey_Assets() )->load_admin_scripts(
201            'jetpack_stats_widget',
202            'widget-loader.min',
203            array(
204                'config_variable_name' => 'jetpackStatsOdysseyWidgetConfigData',
205                'config_data'          => $config_data,
206                'enqueue_css'          => false,
207            )
208        );
209    }
210
211    /**
212     * Load the admin scripts when the widget is visible.
213     */
214    public function maybe_load_admin_scripts() {
215        if ( $this->is_widget_hidden() ) {
216            return;
217        }
218        $this->load_admin_scripts();
219    }
220
221    /**
222     * Returns true if the widget is hidden for the current screen and current user.
223     *
224     * @return bool
225     */
226    public function is_widget_hidden() {
227        $hidden = get_hidden_meta_boxes( get_current_screen() );
228        return in_array( self::DASHBOARD_WIDGET_ID, $hidden, true );
229    }
230}