foreman_remote_execution 14.1.4 → 15.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/assets/javascripts/foreman_remote_execution/locale/de/foreman_remote_execution.js +178 -13
- data/app/assets/javascripts/foreman_remote_execution/locale/en/foreman_remote_execution.js +1832 -2
- data/app/assets/javascripts/foreman_remote_execution/locale/en_GB/foreman_remote_execution.js +176 -11
- data/app/assets/javascripts/foreman_remote_execution/locale/es/foreman_remote_execution.js +178 -13
- data/app/assets/javascripts/foreman_remote_execution/locale/fr/foreman_remote_execution.js +179 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/ja/foreman_remote_execution.js +179 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/ka/foreman_remote_execution.js +178 -13
- data/app/assets/javascripts/foreman_remote_execution/locale/ko/foreman_remote_execution.js +449 -285
- data/app/assets/javascripts/foreman_remote_execution/locale/pt_BR/foreman_remote_execution.js +178 -13
- data/app/assets/javascripts/foreman_remote_execution/locale/ru/foreman_remote_execution.js +177 -12
- data/app/assets/javascripts/foreman_remote_execution/locale/zh_CN/foreman_remote_execution.js +179 -14
- data/app/assets/javascripts/foreman_remote_execution/locale/zh_TW/foreman_remote_execution.js +177 -12
- data/app/controllers/api/v2/job_invocations_controller.rb +8 -0
- data/app/controllers/job_invocations_controller.rb +1 -1
- data/app/controllers/template_invocations_controller.rb +57 -0
- data/app/controllers/ui_job_wizard_controller.rb +6 -3
- data/app/helpers/remote_execution_helper.rb +5 -6
- data/app/views/api/v2/job_invocations/base.json.rabl +1 -1
- data/app/views/api/v2/job_invocations/hosts.json.rabl +1 -1
- data/app/views/api/v2/job_invocations/main.json.rabl +1 -1
- data/app/views/api/v2/job_invocations/show.json.rabl +18 -0
- data/app/views/templates/script/convert2rhel_analyze.erb +4 -4
- data/config/routes.rb +2 -0
- data/lib/foreman_remote_execution/engine.rb +3 -3
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/locale/de/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/de/foreman_remote_execution.po +178 -13
- data/locale/en/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/en/foreman_remote_execution.po +1840 -0
- data/locale/en_GB/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/en_GB/foreman_remote_execution.po +176 -11
- data/locale/es/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/es/foreman_remote_execution.po +178 -13
- data/locale/foreman_remote_execution.pot +470 -184
- data/locale/fr/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/fr/foreman_remote_execution.po +179 -14
- data/locale/ja/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ja/foreman_remote_execution.po +179 -14
- data/locale/ka/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ka/foreman_remote_execution.po +178 -13
- data/locale/ko/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ko/foreman_remote_execution.po +449 -285
- data/locale/pt_BR/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/pt_BR/foreman_remote_execution.po +178 -13
- data/locale/ru/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/ru/foreman_remote_execution.po +177 -12
- data/locale/zh_CN/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/zh_CN/foreman_remote_execution.po +179 -14
- data/locale/zh_TW/LC_MESSAGES/foreman_remote_execution.mo +0 -0
- data/locale/zh_TW/foreman_remote_execution.po +177 -12
- data/webpack/JobInvocationDetail/JobAdditionInfo.js +214 -0
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +40 -2
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +70 -0
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +177 -80
- data/webpack/JobInvocationDetail/JobInvocationHostTableToolbar.js +63 -0
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +8 -1
- data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +61 -10
- data/webpack/JobInvocationDetail/OpenAlInvocations.js +111 -0
- data/webpack/JobInvocationDetail/TemplateInvocation.js +202 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +124 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputToggleGroup.js +156 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/PreviewTemplate.js +50 -0
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +224 -0
- data/webpack/JobInvocationDetail/TemplateInvocationPage.js +53 -0
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +1 -1
- data/webpack/JobInvocationDetail/__tests__/OpenAlInvocations.test.js +110 -0
- data/webpack/JobInvocationDetail/__tests__/OutputCodeBlock.test.js +69 -0
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocation.test.js +131 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +130 -0
- data/webpack/JobInvocationDetail/index.js +18 -3
- data/webpack/JobWizard/JobWizard.js +38 -16
- data/webpack/JobWizard/{StartsBeforeErrorAlert.js → StartsErrorAlert.js} +16 -1
- data/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +1 -1
- data/webpack/JobWizard/steps/Schedule/ScheduleFuture.js +1 -1
- data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +5 -3
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +1 -1
- data/webpack/JobWizard/steps/Schedule/__tests__/Schedule.test.js +3 -3
- data/webpack/JobWizard/steps/form/DateTimePicker.js +13 -0
- data/webpack/JobWizard/steps/form/Formatter.js +1 -0
- data/webpack/JobWizard/steps/form/ResourceSelect.js +34 -9
- data/webpack/Routes/routes.js +6 -0
- data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext/index.js +1 -0
- data/webpack/react_app/components/RegistrationExtension/RexPull.js +27 -2
- data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +1 -1
- metadata +16 -7
@@ -0,0 +1,214 @@
|
|
1
|
+
/* eslint-disable max-lines */
|
2
|
+
/* eslint-disable camelcase */
|
3
|
+
import React, { useState } from 'react';
|
4
|
+
import PropTypes from 'prop-types';
|
5
|
+
import {
|
6
|
+
ExpandableSection,
|
7
|
+
DataList,
|
8
|
+
DataListCell,
|
9
|
+
DataListItemCells,
|
10
|
+
DataListItem,
|
11
|
+
DataListItemRow,
|
12
|
+
} from '@patternfly/react-core';
|
13
|
+
import { translate as __ } from 'foremanReact/common/I18n';
|
14
|
+
import { TARGETING_TYPES } from './JobInvocationConstants';
|
15
|
+
|
16
|
+
const ItemsParser = ({ items }) => (
|
17
|
+
<>
|
18
|
+
{items.map(
|
19
|
+
({ title, value, wrappedValue }, index) =>
|
20
|
+
value && (
|
21
|
+
<DataListItem key={index}>
|
22
|
+
<DataListItemRow>
|
23
|
+
<DataListItemCells
|
24
|
+
dataListCells={[
|
25
|
+
<DataListCell width={1} key={0}>
|
26
|
+
{title}
|
27
|
+
</DataListCell>,
|
28
|
+
<DataListCell width={4} key={1}>
|
29
|
+
{wrappedValue || value}
|
30
|
+
</DataListCell>,
|
31
|
+
]}
|
32
|
+
/>
|
33
|
+
</DataListItemRow>
|
34
|
+
</DataListItem>
|
35
|
+
)
|
36
|
+
)}
|
37
|
+
</>
|
38
|
+
);
|
39
|
+
const Schedule = ({ data }) => {
|
40
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
41
|
+
const {
|
42
|
+
concurrency_level,
|
43
|
+
scheduling,
|
44
|
+
time_to_pickup,
|
45
|
+
execution_timeout_interval,
|
46
|
+
} = data;
|
47
|
+
if (
|
48
|
+
!concurrency_level &&
|
49
|
+
!scheduling &&
|
50
|
+
!time_to_pickup &&
|
51
|
+
!execution_timeout_interval
|
52
|
+
)
|
53
|
+
return null;
|
54
|
+
const items = [
|
55
|
+
{ title: __('Concurrency level limited to'), value: concurrency_level },
|
56
|
+
{ title: __('Scheduled to start before'), value: scheduling?.start_before },
|
57
|
+
{ title: __('Scheduled to start at'), value: scheduling?.start_at },
|
58
|
+
{
|
59
|
+
title: __('Timeout to kill after'),
|
60
|
+
value: execution_timeout_interval,
|
61
|
+
wrappedValue: `${execution_timeout_interval} ${__('seconds')}`,
|
62
|
+
},
|
63
|
+
{
|
64
|
+
title: __('Time to pickup'),
|
65
|
+
value: time_to_pickup,
|
66
|
+
wrappedValue: `${time_to_pickup} ${__('seconds')}`,
|
67
|
+
},
|
68
|
+
];
|
69
|
+
return (
|
70
|
+
<ExpandableSection
|
71
|
+
toggleText={__('Schedule')}
|
72
|
+
onToggle={setIsExpanded}
|
73
|
+
isExpanded={isExpanded}
|
74
|
+
>
|
75
|
+
<DataList isCompact>
|
76
|
+
<ItemsParser items={items} />
|
77
|
+
</DataList>
|
78
|
+
</ExpandableSection>
|
79
|
+
);
|
80
|
+
};
|
81
|
+
const Recurring = ({ data }) => {
|
82
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
83
|
+
const { recurrence } = data;
|
84
|
+
|
85
|
+
if (!recurrence) return null;
|
86
|
+
const items = [
|
87
|
+
{
|
88
|
+
title: __('ID'),
|
89
|
+
value: recurrence.id,
|
90
|
+
wrappedValue: (
|
91
|
+
<a href={`/foreman_tasks/recurring_logics/${recurrence.id}`}>
|
92
|
+
{recurrence.id}
|
93
|
+
</a>
|
94
|
+
),
|
95
|
+
},
|
96
|
+
{ title: __('Cron line'), value: recurrence.cron_line },
|
97
|
+
// TODO { title:__('Action') , value: {format_task_input(recurring_logic.tasks.last)} },
|
98
|
+
{ title: __('Last occurrence'), value: recurrence.last_occurrence },
|
99
|
+
{ title: __('Next occurrence'), value: recurrence.next_occurrence },
|
100
|
+
{ title: __('Current iteration'), value: recurrence.iteration },
|
101
|
+
{ title: __('Iteration limit'), value: recurrence.max_iteration },
|
102
|
+
{ title: __('Repeat until'), value: recurrence.end_time },
|
103
|
+
// TODO { title:__('State') , value: {recurring_logic_state(recurring_logic)} },
|
104
|
+
{ title: __('Purpose'), value: recurrence.purpose },
|
105
|
+
{ title: __('Task count'), value: recurrence.task_count },
|
106
|
+
];
|
107
|
+
return (
|
108
|
+
<ExpandableSection
|
109
|
+
toggleText={__('Recurring logic')}
|
110
|
+
onToggle={setIsExpanded}
|
111
|
+
isExpanded={isExpanded}
|
112
|
+
>
|
113
|
+
<DataList isCompact>
|
114
|
+
<ItemsParser items={items} />
|
115
|
+
</DataList>
|
116
|
+
</ExpandableSection>
|
117
|
+
);
|
118
|
+
};
|
119
|
+
const TargetHosts = ({ data }) => {
|
120
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
121
|
+
const {
|
122
|
+
targeting,
|
123
|
+
job_location: location,
|
124
|
+
job_organization: organization,
|
125
|
+
} = data;
|
126
|
+
|
127
|
+
const targetingSelectioning = targeting.bookmark_name
|
128
|
+
? `${__('Bookmark')} ${targeting.bookmark_name}`
|
129
|
+
: __('Manual selection');
|
130
|
+
const items = [
|
131
|
+
{
|
132
|
+
title: __('Organization'),
|
133
|
+
value: true,
|
134
|
+
wrappedValue: organization || __('Any organization'),
|
135
|
+
},
|
136
|
+
{
|
137
|
+
title: __('Location'),
|
138
|
+
value: true,
|
139
|
+
wrappedValue: location || __('Any location'),
|
140
|
+
},
|
141
|
+
{
|
142
|
+
title: __('Execution order'),
|
143
|
+
value: true,
|
144
|
+
wrappedValue: targeting.randomized_ordering
|
145
|
+
? __('Randomized')
|
146
|
+
: __('Alphabetical'),
|
147
|
+
},
|
148
|
+
];
|
149
|
+
return (
|
150
|
+
<ExpandableSection
|
151
|
+
toggleText={__('Target Hosts')}
|
152
|
+
onToggle={setIsExpanded}
|
153
|
+
isExpanded={isExpanded}
|
154
|
+
>
|
155
|
+
<span>{targetingSelectioning}</span>{' '}
|
156
|
+
<span>
|
157
|
+
{__('using ')}
|
158
|
+
<b>{TARGETING_TYPES[targeting.targeting_type].toLowerCase()}</b>
|
159
|
+
</span>
|
160
|
+
<pre>{targeting.search_query}</pre>
|
161
|
+
<DataList isCompact>
|
162
|
+
<ItemsParser items={items} />
|
163
|
+
</DataList>
|
164
|
+
</ExpandableSection>
|
165
|
+
);
|
166
|
+
};
|
167
|
+
const Inputs = ({ data }) => {
|
168
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
169
|
+
const inputs =
|
170
|
+
data?.pattern_template_invocations?.[0]?.template_invocation_input_values;
|
171
|
+
|
172
|
+
if (!inputs) return null;
|
173
|
+
return (
|
174
|
+
<ExpandableSection
|
175
|
+
toggleText={__('User Inputs')}
|
176
|
+
onToggle={setIsExpanded}
|
177
|
+
isExpanded={isExpanded}
|
178
|
+
>
|
179
|
+
<DataList isCompact>
|
180
|
+
<ItemsParser
|
181
|
+
items={inputs.map(({ template_input_name: title, value }) => ({
|
182
|
+
title,
|
183
|
+
value: true,
|
184
|
+
wrappedValue: value,
|
185
|
+
}))}
|
186
|
+
/>
|
187
|
+
</DataList>
|
188
|
+
</ExpandableSection>
|
189
|
+
);
|
190
|
+
};
|
191
|
+
export const JobAdditionInfo = ({ data }) => (
|
192
|
+
<>
|
193
|
+
<Recurring data={data} />
|
194
|
+
<TargetHosts data={data} />
|
195
|
+
<Inputs data={data} />
|
196
|
+
<Schedule data={data} />
|
197
|
+
</>
|
198
|
+
);
|
199
|
+
|
200
|
+
JobAdditionInfo.propTypes = {
|
201
|
+
data: PropTypes.shape({
|
202
|
+
recurrence: PropTypes.object,
|
203
|
+
targeting: PropTypes.object,
|
204
|
+
}).isRequired,
|
205
|
+
};
|
206
|
+
|
207
|
+
Recurring.propTypes = JobAdditionInfo.propTypes;
|
208
|
+
TargetHosts.propTypes = JobAdditionInfo.propTypes;
|
209
|
+
Inputs.propTypes = JobAdditionInfo.propTypes;
|
210
|
+
Schedule.propTypes = JobAdditionInfo.propTypes;
|
211
|
+
|
212
|
+
ItemsParser.propTypes = {
|
213
|
+
items: PropTypes.array.isRequired,
|
214
|
+
};
|
@@ -19,6 +19,15 @@ export const JOB_INVOCATION_HOSTS = 'JOB_INVOCATION_HOSTS';
|
|
19
19
|
export const currentPermissionsUrl = foremanUrl(
|
20
20
|
'/api/v2/permissions/current_permissions'
|
21
21
|
);
|
22
|
+
export const GET_TEMPLATE_INVOCATION = 'GET_TEMPLATE_INVOCATION';
|
23
|
+
export const showTemplateInvocationUrl = (hostID, jobID) =>
|
24
|
+
`/show_template_invocation_by_host/${hostID}/job_invocation/${jobID}`;
|
25
|
+
|
26
|
+
export const templateInvocationPageUrl = (hostID, jobID) =>
|
27
|
+
`/job_invocations_detail/${jobID}/host_invocation/${hostID}`;
|
28
|
+
|
29
|
+
export const jobInvocationDetailsUrl = id =>
|
30
|
+
`/experimental/job_invocations_detail/${id}`;
|
22
31
|
|
23
32
|
export const STATUS = {
|
24
33
|
PENDING: 'pending',
|
@@ -33,6 +42,14 @@ export const STATUS_UPPERCASE = {
|
|
33
42
|
PENDING: 'PENDING',
|
34
43
|
};
|
35
44
|
|
45
|
+
export const STATUS_TITLES = {
|
46
|
+
ALL_STATUSES: { id: 'all_statuses', title: __('All statuses') },
|
47
|
+
SUCCESS: { id: 'success', title: __('Succeeded') },
|
48
|
+
FAILED: { id: 'failed', title: __('Failed') },
|
49
|
+
PENDING: { id: 'pending', title: __('In Progress') },
|
50
|
+
CANCELLED: { id: 'cancelled', title: __('Cancelled') },
|
51
|
+
};
|
52
|
+
|
36
53
|
export const DATE_OPTIONS = {
|
37
54
|
day: 'numeric',
|
38
55
|
month: 'short',
|
@@ -52,7 +69,7 @@ const Columns = () => {
|
|
52
69
|
return { title: __('Failed'), status: 1 };
|
53
70
|
case 'planned':
|
54
71
|
return { title: __('Scheduled'), status: 2 };
|
55
|
-
case 'running':
|
72
|
+
case 'running' || 'pending':
|
56
73
|
return { title: __('Pending'), status: 3 };
|
57
74
|
case 'cancelled':
|
58
75
|
return { title: __('Cancelled'), status: 4 };
|
@@ -65,18 +82,25 @@ const Columns = () => {
|
|
65
82
|
const hostDetailsPageUrl = useForemanHostDetailsPageUrl();
|
66
83
|
|
67
84
|
return {
|
85
|
+
expand: {
|
86
|
+
title: '',
|
87
|
+
weight: 0,
|
88
|
+
wrapper: () => null,
|
89
|
+
},
|
68
90
|
name: {
|
69
91
|
title: __('Name'),
|
70
92
|
wrapper: ({ name }) => (
|
71
93
|
<a href={`${hostDetailsPageUrl}${name}`}>{name}</a>
|
72
94
|
),
|
95
|
+
isSorted: true,
|
73
96
|
weight: 1,
|
74
97
|
},
|
75
|
-
|
98
|
+
hostgroup: {
|
76
99
|
title: __('Host group'),
|
77
100
|
wrapper: ({ hostgroup_id, hostgroup_name }) => (
|
78
101
|
<a href={`/hostgroups/${hostgroup_id}/edit`}>{hostgroup_name}</a>
|
79
102
|
),
|
103
|
+
isSorted: true,
|
80
104
|
weight: 2,
|
81
105
|
},
|
82
106
|
os: {
|
@@ -86,6 +110,7 @@ const Columns = () => {
|
|
86
110
|
{operatingsystem_name}
|
87
111
|
</a>
|
88
112
|
),
|
113
|
+
isSorted: true,
|
89
114
|
weight: 3,
|
90
115
|
},
|
91
116
|
smart_proxy: {
|
@@ -93,6 +118,7 @@ const Columns = () => {
|
|
93
118
|
wrapper: ({ smart_proxy_name, smart_proxy_id }) => (
|
94
119
|
<a href={`/smart_proxies/${smart_proxy_id}`}>{smart_proxy_name}</a>
|
95
120
|
),
|
121
|
+
isSorted: true,
|
96
122
|
weight: 4,
|
97
123
|
},
|
98
124
|
status: {
|
@@ -109,7 +135,19 @@ const Columns = () => {
|
|
109
135
|
},
|
110
136
|
weight: 5,
|
111
137
|
},
|
138
|
+
actions: {
|
139
|
+
title: '',
|
140
|
+
weight: 6,
|
141
|
+
wrapper: () => null,
|
142
|
+
},
|
112
143
|
};
|
113
144
|
};
|
114
145
|
|
115
146
|
export default Columns;
|
147
|
+
|
148
|
+
const STATIC_TYPE = 'static_query';
|
149
|
+
const DYNAMIC_TYPE = 'dynamic_query';
|
150
|
+
export const TARGETING_TYPES = {
|
151
|
+
[STATIC_TYPE]: __('Static Query'),
|
152
|
+
[DYNAMIC_TYPE]: __('Dynamic Query'),
|
153
|
+
};
|
@@ -38,3 +38,73 @@
|
|
38
38
|
height: $chart_size;
|
39
39
|
}
|
40
40
|
}
|
41
|
+
.job-additional-info {
|
42
|
+
padding: 0;
|
43
|
+
margin-bottom: -10px;
|
44
|
+
}
|
45
|
+
.job-details-table-section {
|
46
|
+
section:nth-child(1) {
|
47
|
+
padding: 0;
|
48
|
+
}
|
49
|
+
}
|
50
|
+
|
51
|
+
.template-invocation {
|
52
|
+
&.output-in-table-view {
|
53
|
+
div.invocation-output {
|
54
|
+
overflow: auto;
|
55
|
+
max-height: 25em;
|
56
|
+
}
|
57
|
+
}
|
58
|
+
div.invocation-output {
|
59
|
+
display: block;
|
60
|
+
padding: 9.5px;
|
61
|
+
margin: 0 0 10px;
|
62
|
+
font-size: 12px;
|
63
|
+
word-break: break-all;
|
64
|
+
word-wrap: break-word;
|
65
|
+
color: rgba(255, 255, 255, 1);
|
66
|
+
background-color: rgba(47, 47, 47, 1);
|
67
|
+
border: 1px solid #000000;
|
68
|
+
border-radius: 0px;
|
69
|
+
font-family: Menlo, Monaco, Consolas, monospace;
|
70
|
+
|
71
|
+
div.printable {
|
72
|
+
min-height: 50px;
|
73
|
+
}
|
74
|
+
|
75
|
+
div.line.stderr,
|
76
|
+
div.line.error,
|
77
|
+
div.line.debug {
|
78
|
+
color: red;
|
79
|
+
}
|
80
|
+
|
81
|
+
div.line span.counter {
|
82
|
+
float: left;
|
83
|
+
clear: left;
|
84
|
+
}
|
85
|
+
|
86
|
+
div.line div.content {
|
87
|
+
position: relative;
|
88
|
+
margin-left: 50px;
|
89
|
+
white-space: pre-wrap;
|
90
|
+
}
|
91
|
+
|
92
|
+
a {
|
93
|
+
color: #ffffff;
|
94
|
+
}
|
95
|
+
|
96
|
+
a.scroll-link{
|
97
|
+
position: relative;
|
98
|
+
bottom: 10px;
|
99
|
+
float: right;
|
100
|
+
}
|
101
|
+
}
|
102
|
+
|
103
|
+
.template-invocation-preview {
|
104
|
+
margin-top: 10px;
|
105
|
+
}
|
106
|
+
|
107
|
+
.pf-c-toggle-group {
|
108
|
+
margin-bottom: 10px;
|
109
|
+
}
|
110
|
+
}
|