foreman_docker 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +10 -1
- data/app/assets/javascripts/foreman_docker/container_image_search.js +203 -0
- data/app/assets/javascripts/foreman_docker/create_registry.js +15 -0
- data/app/assets/stylesheets/foreman_docker/autocomplete.css.scss +4 -0
- data/app/controllers/concerns/foreman/controller/parameters/docker_registry.rb +1 -1
- data/app/controllers/image_search_controller.rb +6 -35
- data/app/helpers/container_steps_helper.rb +0 -1
- data/app/models/container.rb +1 -1
- data/app/models/docker_container_wizard_state.rb +1 -1
- data/app/models/docker_container_wizard_states/configuration.rb +1 -1
- data/app/models/docker_container_wizard_states/environment.rb +1 -1
- data/app/models/docker_container_wizard_states/image.rb +1 -1
- data/app/models/docker_container_wizard_states/preliminary.rb +1 -1
- data/app/models/docker_parameter.rb +1 -1
- data/app/models/docker_registry.rb +8 -4
- data/app/models/foreman_docker/docker.rb +10 -2
- data/app/models/service/registry_api.rb +10 -5
- data/app/views/containers/steps/_form_buttons.html.erb +5 -7
- data/app/views/containers/steps/_image_hub_tab.html.erb +16 -17
- data/app/views/containers/steps/image.html.erb +1 -3
- data/app/views/image_search/_repository_search_results.html.erb +1 -1
- data/app/views/registries/_form.html.erb +5 -3
- data/app/views/registries/index.html.erb +11 -10
- data/db/migrate/20170508130316_add_verify_ssl_option_to_docker_registries.rb +5 -0
- data/lib/foreman_docker/engine.rb +7 -6
- data/lib/foreman_docker/version.rb +1 -1
- data/test/functionals/image_search_controller_test.rb +120 -169
- data/test/integration/container_steps_test.rb +118 -18
- data/test/integration/registry_creation_test.rb +19 -0
- data/test/units/docker_container_wizard_states/image_test.rb +0 -1
- data/test/units/docker_registry_test.rb +0 -8
- data/test/units/registry_api_test.rb +13 -1
- metadata +10 -6
- data/app/assets/javascripts/foreman_docker/image_step.js +0 -175
@@ -1,24 +1,124 @@
|
|
1
1
|
require 'integration_test_helper'
|
2
2
|
|
3
|
-
class ContainerStepsTest <
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
class ContainerStepsTest < IntegrationTestWithJavascript
|
4
|
+
let(:wizard_state) { DockerContainerWizardState.create! }
|
5
|
+
let(:compute_resource) { FactoryGirl.create(:docker_cr) }
|
6
|
+
let(:registry) { FactoryGirl.create(:docker_registry) }
|
7
|
+
let(:image_search_service) { ForemanDocker::ImageSearch.new }
|
8
|
+
let(:image_search_results) do
|
9
|
+
[{ 'name' => 'my_fake_repository_result',
|
10
|
+
'star_count' => 300,
|
11
|
+
'description' => 'fake repository' }]
|
7
12
|
end
|
8
13
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
14
|
+
let(:preliminary) do
|
15
|
+
DockerContainerWizardStates::Preliminary.create!(
|
16
|
+
:wizard_state => wizard_state,
|
17
|
+
:compute_resource => compute_resource
|
18
|
+
)
|
19
|
+
end
|
20
|
+
|
21
|
+
setup do
|
22
|
+
stub_image_existance
|
23
|
+
stub_registry_api
|
24
|
+
ImageSearchController.any_instance.stubs(:image_search_service)
|
25
|
+
.returns(image_search_service)
|
26
|
+
end
|
27
|
+
|
28
|
+
describe 'on preliminary step' do
|
29
|
+
test 'it shows docker compute resources' do
|
30
|
+
compute_resource.save
|
31
|
+
visit wizard_state_step_path(:wizard_state_id => wizard_state, :id => :preliminary)
|
32
|
+
assert_text compute_resource.name
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when no compute resources are available' do
|
36
|
+
setup do
|
37
|
+
ForemanDocker::Docker.destroy_all
|
38
|
+
end
|
39
|
+
|
40
|
+
test 'shows a link to a new compute resource if none is available' do
|
41
|
+
visit wizard_state_step_path(:wizard_state_id => wizard_state, :id => :preliminary)
|
42
|
+
assert has_selector?("div.alert", :text => 'Please add a new one')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
test 'shows taxonomies tabs' do
|
47
|
+
visit wizard_state_step_path(:wizard_state_id => wizard_state, :id => :preliminary)
|
48
|
+
assert has_selector?("a", :text => 'Locations') if SETTINGS[:locations_enabled]
|
49
|
+
assert has_selector?("a", :text => 'Organizations') if SETTINGS[:organizations_enabled]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe 'on image step' do
|
54
|
+
['hub', 'registry'].each do |tab|
|
55
|
+
describe "on #{tab} tab" do
|
56
|
+
let(:image_field_id) { "#{tab}_docker_container_wizard_states_image_repository_name" }
|
57
|
+
let(:tag_field_id) { "#{tab}_docker_container_wizard_states_image_tag" }
|
58
|
+
|
59
|
+
setup do
|
60
|
+
registry.save
|
61
|
+
wizard_state.preliminary = preliminary
|
62
|
+
visit wizard_state_step_path(:wizard_state_id => wizard_state, :id => :image)
|
63
|
+
find("##{tab}_tab").click
|
64
|
+
|
65
|
+
if tab == 'registry'
|
66
|
+
find("#docker_container_wizard_states_image_registry_id option:last-child", :visible => false)
|
67
|
+
.select_option
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
test 'clicking on search loads repositories' do
|
72
|
+
image_search_service.expects(:search).at_least(2)
|
73
|
+
.returns(image_search_results)
|
74
|
+
fill_in image_field_id, :with => "fake"
|
75
|
+
wait_for_ajax
|
76
|
+
find("#search_repository_button_#{tab}").click
|
77
|
+
wait_for_ajax
|
78
|
+
assert_text 'my_fake_repository_result'
|
79
|
+
end
|
80
|
+
|
81
|
+
describe 'autcomplete' do
|
82
|
+
describe 'for image name' do
|
83
|
+
test 'entering text triggers autocomplete' do
|
84
|
+
image_search_service.expects(:search)
|
85
|
+
.returns(image_search_results)
|
86
|
+
fill_in image_field_id, :with => 'fa'
|
87
|
+
wait_for_ajax
|
88
|
+
has_selector?('.ui-autocomplete a', :text => 'my_fake_repository_result')
|
89
|
+
end
|
90
|
+
|
91
|
+
if tab == 'registry'
|
92
|
+
context 'when no registry is selected' do
|
93
|
+
test 'it does not trigger a autocomplete' do
|
94
|
+
image_search_service.expects(:search).never
|
95
|
+
find("#docker_container_wizard_states_image_registry_id option:first-child", :visible => false).select_option
|
96
|
+
fill_in image_field_id, :with => 'fa'
|
97
|
+
wait_for_ajax
|
98
|
+
has_no_selector?('.ui-autocomplete a', :text => 'my_fake_repository_result')
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe 'for tag' do
|
105
|
+
let(:image_name) { 'fake' }
|
106
|
+
let(:tag_snippet) { 'lat' }
|
107
|
+
|
108
|
+
setup do
|
109
|
+
fill_in image_field_id, :with => 'fake'
|
110
|
+
end
|
111
|
+
|
112
|
+
test 'it searches for tags' do
|
113
|
+
image_search_service.expects(:search).at_least(2)
|
114
|
+
.returns(['latest'])
|
115
|
+
fill_in tag_field_id, :with => tag_snippet
|
116
|
+
wait_for_ajax
|
117
|
+
has_selector?('.ui-autocomplete a', :text => 'latest')
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
13
123
|
end
|
14
|
-
# test 'clicking on search loads repositories' do
|
15
|
-
# Capybara.javascript_driver = :webkit
|
16
|
-
# container = FactoryGirl.create(:container)
|
17
|
-
# visit container_step_path(:container_id => container.id, :id => :repository)
|
18
|
-
# ComputeResource.any_instance.expects(:search).returns([{'name' => 'my_fake_repository_result',
|
19
|
-
# 'star_count' => 300,
|
20
|
-
# 'description' => 'fake repository'}])
|
21
|
-
# click_button 'search_repository'
|
22
|
-
# assert has_link? 'my_fake_repository_result'
|
23
|
-
# end
|
24
124
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'integration_test_helper'
|
2
|
+
|
3
|
+
class RegistryiCreationTest < IntegrationTestWithJavascript
|
4
|
+
let(:registry_values) { FactoryGirl.build(:docker_registry) }
|
5
|
+
|
6
|
+
setup do
|
7
|
+
DockerRegistry.any_instance.stubs(:attempt_login).returns(true)
|
8
|
+
visit new_registry_path
|
9
|
+
end
|
10
|
+
|
11
|
+
test 'can create a registy' do
|
12
|
+
assert_difference('DockerRegistry.count') do
|
13
|
+
fill_in 'docker_registry_name', with: registry_values.name
|
14
|
+
fill_in 'docker_registry_url', with: registry_values.url
|
15
|
+
page.find('#new_docker_registry .btn-primary').click
|
16
|
+
wait_for_ajax
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -32,7 +32,6 @@ module DockerContainerWizardStates
|
|
32
32
|
Service::RegistryApi.stubs(:docker_hub).returns(docker_hub)
|
33
33
|
end
|
34
34
|
|
35
|
-
|
36
35
|
describe 'it validates that the image is available' do
|
37
36
|
test 'validates via the image_search_service' do
|
38
37
|
DockerContainerWizardStates::Image.any_instance
|
@@ -55,12 +55,4 @@ class DockerRegistryTest < ActiveSupport::TestCase
|
|
55
55
|
assert_kind_of Service::RegistryApi, api
|
56
56
|
end
|
57
57
|
end
|
58
|
-
|
59
|
-
describe '#api' do
|
60
|
-
let(:api) { subject.api }
|
61
|
-
|
62
|
-
test 'returns a RegistryApi instance' do
|
63
|
-
assert_kind_of Service::RegistryApi, api
|
64
|
-
end
|
65
|
-
end
|
66
58
|
end
|
@@ -30,6 +30,15 @@ class RegistryApiTest < ActiveSupport::TestCase
|
|
30
30
|
assert_equal password, subject.connection.options[:password]
|
31
31
|
end
|
32
32
|
end
|
33
|
+
|
34
|
+
context 'verify ssl is set' do
|
35
|
+
let(:verify_ssl) { [true, false].sample }
|
36
|
+
subject { Service::RegistryApi.new(url: url, verify_ssl: verify_ssl) }
|
37
|
+
|
38
|
+
test 'it passes it as ssl_verify_peer' do
|
39
|
+
assert_equal verify_ssl, subject.connection.options[:ssl_verify_peer]
|
40
|
+
end
|
41
|
+
end
|
33
42
|
end
|
34
43
|
|
35
44
|
describe '#get' do
|
@@ -185,8 +194,11 @@ class RegistryApiTest < ActiveSupport::TestCase
|
|
185
194
|
|
186
195
|
describe '#ok?' do
|
187
196
|
test 'calls the API via #get with /v1/' do
|
197
|
+
params = subject.send(:default_connection_options)
|
198
|
+
.merge(subject.send(:credentials))
|
199
|
+
.merge(path: '/v1/')
|
188
200
|
subject.connection.expects(:get)
|
189
|
-
.with('/', nil,
|
201
|
+
.with('/', nil, params)
|
190
202
|
.returns('Docker Registry API')
|
191
203
|
assert subject.ok?
|
192
204
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: foreman_docker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Daniel Lobato, Amos Benari
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-06-
|
11
|
+
date: 2017-06-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: docker-api
|
@@ -72,14 +72,14 @@ dependencies:
|
|
72
72
|
requirements:
|
73
73
|
- - '='
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version: 0.
|
75
|
+
version: 0.49.1
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - '='
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version: 0.
|
82
|
+
version: 0.49.1
|
83
83
|
description: Provision and manage Docker containers and images from Foreman.
|
84
84
|
email:
|
85
85
|
- dlobatog@redhat.com, abenari@redhat.com
|
@@ -91,7 +91,8 @@ files:
|
|
91
91
|
- LICENSE
|
92
92
|
- README.md
|
93
93
|
- Rakefile
|
94
|
-
- app/assets/javascripts/foreman_docker/
|
94
|
+
- app/assets/javascripts/foreman_docker/container_image_search.js
|
95
|
+
- app/assets/javascripts/foreman_docker/create_registry.js
|
95
96
|
- app/assets/stylesheets/foreman_docker/autocomplete.css.scss
|
96
97
|
- app/assets/stylesheets/foreman_docker/terminal.css.scss
|
97
98
|
- app/controllers/api/v2/containers_controller.rb
|
@@ -189,6 +190,7 @@ files:
|
|
189
190
|
- db/migrate/20150814205620_change_container_column_type.rb
|
190
191
|
- db/migrate/20160605133025_create_docker_parameters.rb
|
191
192
|
- db/migrate/20160605134652_move_parameters_to_docker_parameters.rb
|
193
|
+
- db/migrate/20170508130316_add_verify_ssl_option_to_docker_registries.rb
|
192
194
|
- lib/foreman_docker.rb
|
193
195
|
- lib/foreman_docker/engine.rb
|
194
196
|
- lib/foreman_docker/version.rb
|
@@ -226,6 +228,7 @@ files:
|
|
226
228
|
- test/functionals/image_search_controller_test.rb
|
227
229
|
- test/integration/container_steps_test.rb
|
228
230
|
- test/integration/container_test.rb
|
231
|
+
- test/integration/registry_creation_test.rb
|
229
232
|
- test/test_plugin_helper.rb
|
230
233
|
- test/units/container_remover_test.rb
|
231
234
|
- test/units/container_test.rb
|
@@ -272,15 +275,16 @@ test_files:
|
|
272
275
|
- test/functionals/image_search_controller_test.rb
|
273
276
|
- test/integration/container_test.rb
|
274
277
|
- test/integration/container_steps_test.rb
|
278
|
+
- test/integration/registry_creation_test.rb
|
275
279
|
- test/units/container_remover_test.rb
|
276
280
|
- test/units/foreman_docker/compute_resource_extensions_test.rb
|
277
281
|
- test/units/foreman_docker/docker_test.rb
|
278
282
|
- test/units/utility_service_test.rb
|
279
283
|
- test/units/container_test.rb
|
280
|
-
- test/units/docker_registry_test.rb
|
281
284
|
- test/units/image_search_service_test.rb
|
282
285
|
- test/units/containers_service_test.rb
|
283
286
|
- test/units/docker_container_wizard_states/image_test.rb
|
287
|
+
- test/units/docker_registry_test.rb
|
284
288
|
- test/units/registry_api_test.rb
|
285
289
|
- test/test_plugin_helper.rb
|
286
290
|
- ".rubocop.yml"
|
@@ -1,175 +0,0 @@
|
|
1
|
-
$(document).ready(function() {
|
2
|
-
setupAutoComplete("hub");
|
3
|
-
setupAutoComplete("registry");
|
4
|
-
$('#hub_tab').click( function() {
|
5
|
-
$('#docker_container_wizard_states_image_registry_id').val('');
|
6
|
-
});
|
7
|
-
});
|
8
|
-
|
9
|
-
function setupAutoComplete(registryType) {
|
10
|
-
var tag = getTag(registryType),
|
11
|
-
repo = getRepo(registryType);
|
12
|
-
|
13
|
-
tag.autocomplete({
|
14
|
-
source: [],
|
15
|
-
autoFocus: true,
|
16
|
-
delay: 500,
|
17
|
-
minLength: 0
|
18
|
-
}).focus( function() {
|
19
|
-
$(this).data("uiAutocomplete").search($(this).val());
|
20
|
-
});
|
21
|
-
|
22
|
-
repo.autocomplete({
|
23
|
-
source: function( request, response ) { autoCompleteRepo(repo); },
|
24
|
-
delay: 500,
|
25
|
-
minLength: 1
|
26
|
-
});
|
27
|
-
}
|
28
|
-
|
29
|
-
function paramsForSearch(registryType) {
|
30
|
-
var image = getRepo(registryType),
|
31
|
-
tag = getTag(registryType),
|
32
|
-
registryId = $('#docker_container_wizard_states_image_registry_id').val(),
|
33
|
-
params = {
|
34
|
-
registry: registryType,
|
35
|
-
search: image.val()
|
36
|
-
}
|
37
|
-
|
38
|
-
if (tag.val() != '') {
|
39
|
-
params.search = image.val() + ':' + tag.val();
|
40
|
-
};
|
41
|
-
|
42
|
-
if (registryType == 'registry' && registryId != '') {
|
43
|
-
params.registry_id = registryId;
|
44
|
-
};
|
45
|
-
|
46
|
-
return params;
|
47
|
-
}
|
48
|
-
|
49
|
-
function autoCompleteRepo(item) {
|
50
|
-
var registryType = $(item).data('registry'),
|
51
|
-
search_add_on = getImageConfirmation(registryType),
|
52
|
-
tag = getTag(registryType),
|
53
|
-
params = paramsForSearch(registryType);
|
54
|
-
|
55
|
-
if (params.search == '' ||
|
56
|
-
(registryType == 'registry' && typeof params.registry_id == 'undefined')) {
|
57
|
-
return;
|
58
|
-
}
|
59
|
-
|
60
|
-
// Patternfly spinner uses 'float: left' and moves it to the left of the
|
61
|
-
// search button. Instead, we use FontAwesome's spinner to keep it at
|
62
|
-
// the right.
|
63
|
-
search_add_on.attr('class', 'fa fa-spin fa-circle-o-notch');
|
64
|
-
|
65
|
-
$.ajax({
|
66
|
-
type:'get',
|
67
|
-
url: $(item).attr('data-url'),
|
68
|
-
data: params,
|
69
|
-
success:function (result) {
|
70
|
-
if(result == 'true'){
|
71
|
-
search_add_on.attr('title', 'Image found in the compute resource');
|
72
|
-
search_add_on.attr('class', 'pficon pficon-ok');
|
73
|
-
setWaitingText('Image found: <strong>' + item.val() + '</strong>. Retrieving available tags, please wait...', registryType);
|
74
|
-
setAutocompleteTags(registryType);
|
75
|
-
} else {
|
76
|
-
search_add_on.attr('title', 'Image NOT found in the compute resource');
|
77
|
-
search_add_on.attr('class', 'pficon pficon-error-circle-o');
|
78
|
-
tag.autocomplete('option', 'source', []);
|
79
|
-
}
|
80
|
-
},
|
81
|
-
error: function(result) {
|
82
|
-
$.jnotify(result.responseText, "error", true);
|
83
|
-
}
|
84
|
-
});
|
85
|
-
}
|
86
|
-
|
87
|
-
function setAutocompleteTags(registryType) {
|
88
|
-
var registryType = registryType,
|
89
|
-
tag = getTag(registryType),
|
90
|
-
source = [];
|
91
|
-
|
92
|
-
tag.addClass('spinner-label');
|
93
|
-
tag.val('');
|
94
|
-
|
95
|
-
$.getJSON( tag.data("url"),
|
96
|
-
paramsForSearch(registryType),
|
97
|
-
function(data) {
|
98
|
-
getSearchSpinner(registryType).hide();
|
99
|
-
tag.removeClass('spinner-label');
|
100
|
-
$.each( data, function(index, value) {
|
101
|
-
source.push({label: value.label, value: value.value});
|
102
|
-
});
|
103
|
-
tag.focus();
|
104
|
-
})
|
105
|
-
.error(function(result) {
|
106
|
-
$.jnotify(result.responseText, "error", true);
|
107
|
-
});
|
108
|
-
tag.autocomplete('option', 'source', source);
|
109
|
-
}
|
110
|
-
|
111
|
-
function searchRepo(item) {
|
112
|
-
var registryType = $(item).data('registry'),
|
113
|
-
results = getRepositorySearchResults(registryType),
|
114
|
-
search = getRepo(registryType),
|
115
|
-
searching_spinner = getSearchSpinner(registryType);
|
116
|
-
setWaitingText('<strong>Searching</strong> in the hub, this can be slow, please wait...', registryType);
|
117
|
-
results.html('');
|
118
|
-
results.show();
|
119
|
-
$.ajax({
|
120
|
-
type:'get',
|
121
|
-
dataType:'text',
|
122
|
-
url: $(item).attr('data-url'),
|
123
|
-
data: paramsForSearch(registryType),
|
124
|
-
success: function (result) {
|
125
|
-
results.html(result);
|
126
|
-
},
|
127
|
-
error: function(result) {
|
128
|
-
$.jnotify(result.responseText, "error", true);
|
129
|
-
},
|
130
|
-
complete: function (result) {
|
131
|
-
searching_spinner.hide();
|
132
|
-
}
|
133
|
-
});
|
134
|
-
}
|
135
|
-
|
136
|
-
function repoSelected(item) {
|
137
|
-
var registryType = "hub";
|
138
|
-
if ($(item).data("hub") !== true) {
|
139
|
-
registryType = "registry";
|
140
|
-
}
|
141
|
-
|
142
|
-
getRepositorySearchResults(registryType).hide();
|
143
|
-
setWaitingText('Image selected: <strong>' + item.text + '</strong>. Retrieving available tags, please wait...', registryType);
|
144
|
-
getRepo(registryType).val(item.text);
|
145
|
-
setAutocompleteTags(registryType);
|
146
|
-
}
|
147
|
-
|
148
|
-
function setWaitingText(string, registryType) {
|
149
|
-
getWaitText(registryType).html(string);
|
150
|
-
getSearchSpinner(registryType).show();
|
151
|
-
}
|
152
|
-
|
153
|
-
function getTag(registryType) {
|
154
|
-
return $('form[data-registry="' + registryType + '"] input[data-tag]:first');
|
155
|
-
}
|
156
|
-
|
157
|
-
function getRepo(registryType) {
|
158
|
-
return $('form[data-registry="' + registryType + '"] input[data-search]:first');
|
159
|
-
}
|
160
|
-
|
161
|
-
function getSearchSpinner(registryType) {
|
162
|
-
return $('form[data-registry="' + registryType + '"] [data-search-spinner]:first');
|
163
|
-
}
|
164
|
-
|
165
|
-
function getRepositorySearchResults(registryType) {
|
166
|
-
return $('form[data-registry="' + registryType + '"] [data-repository-search-results]:first');
|
167
|
-
}
|
168
|
-
|
169
|
-
function getImageConfirmation(registryType) {
|
170
|
-
return $('form[data-registry="' + registryType + '"] #image-confirmation');
|
171
|
-
}
|
172
|
-
|
173
|
-
function getWaitText(registryType) {
|
174
|
-
return $('form[data-registry="' + registryType + '"] [data-wait-text]:first');
|
175
|
-
}
|