Merge ~pappacena/launchpad:ui-ocirecipe-args into launchpad:master
- Git
- lp:~pappacena/launchpad
- ui-ocirecipe-args
- Merge into master
Proposed by
Thiago F. Pappacena
Status: | Merged |
---|---|
Approved by: | Thiago F. Pappacena |
Approved revision: | 04c9ed5f8bab13f5c0485f6d41fa38607248c295 |
Merge reported by: | Otto Co-Pilot |
Merged at revision: | not available |
Proposed branch: | ~pappacena/launchpad:ui-ocirecipe-args |
Merge into: | launchpad:master |
Prerequisite: | ~pappacena/launchpad:ocirecipe-args |
Diff against target: |
302 lines (+166/-6) 4 files modified
lib/lp/oci/browser/ocirecipe.py (+59/-6) lib/lp/oci/browser/tests/test_ocirecipe.py (+92/-0) lib/lp/oci/templates/ocirecipe-index.pt (+12/-0) lib/lp/oci/templates/ocirecipe-new.pt (+3/-0) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Colin Watson (community) | Approve | ||
Review via email: mp+389931@code.launchpad.net |
This proposal supersedes a proposal from 2020-08-27.
Commit message
UI to manage build ARGs for OCI recipes
Description of the change
To post a comment you must log in.
- 6919ec2... by Thiago F. Pappacena
-
Merge branch 'ocirecipe-args' into ui-ocirecipe-args
Revision history for this message
Thiago F. Pappacena (pappacena) wrote : | # |
Revision history for this message
Colin Watson (cjwatson) : | # |
review:
Approve
- 7c3074c... by Thiago F. Pappacena
-
Merge branch 'ocirecipe-args' into ui-ocirecipe-args
Revision history for this message
Tom Wardill (twom) : | # |
- 0c1d56d... by Thiago F. Pappacena
-
Small adjustments on build ARG presentation
Revision history for this message
Thiago F. Pappacena (pappacena) : | # |
Revision history for this message
Colin Watson (cjwatson) : | # |
Revision history for this message
Colin Watson (cjwatson) : | # |
- 2fb9015... by Thiago F. Pappacena
-
Fixing tag placement
- 04c9ed5... by Thiago F. Pappacena
-
Merge branch 'ocirecipe-args' into ui-ocirecipe-args
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/lib/lp/oci/browser/ocirecipe.py b/lib/lp/oci/browser/ocirecipe.py | |||
2 | index 5e6a50f..cf11bfb 100644 | |||
3 | --- a/lib/lp/oci/browser/ocirecipe.py | |||
4 | +++ b/lib/lp/oci/browser/ocirecipe.py | |||
5 | @@ -23,9 +23,12 @@ from lazr.restful.interface import ( | |||
6 | 23 | copy_field, | 23 | copy_field, |
7 | 24 | use_template, | 24 | use_template, |
8 | 25 | ) | 25 | ) |
9 | 26 | import six | ||
10 | 26 | from zope.component import getUtility | 27 | from zope.component import getUtility |
11 | 27 | from zope.formlib.form import FormFields | 28 | from zope.formlib.form import FormFields |
12 | 29 | from zope.formlib.textwidgets import TextAreaWidget | ||
13 | 28 | from zope.formlib.widget import ( | 30 | from zope.formlib.widget import ( |
14 | 31 | CustomWidgetFactory, | ||
15 | 29 | DisplayWidget, | 32 | DisplayWidget, |
16 | 30 | renderElement, | 33 | renderElement, |
17 | 31 | ) | 34 | ) |
18 | @@ -35,6 +38,7 @@ from zope.schema import ( | |||
19 | 35 | Choice, | 38 | Choice, |
20 | 36 | List, | 39 | List, |
21 | 37 | Password, | 40 | Password, |
22 | 41 | Text, | ||
23 | 38 | TextLine, | 42 | TextLine, |
24 | 39 | ValidationError, | 43 | ValidationError, |
25 | 40 | ) | 44 | ) |
26 | @@ -242,6 +246,12 @@ class OCIRecipeView(LaunchpadView): | |||
27 | 242 | else: | 246 | else: |
28 | 243 | return "Built on request" | 247 | return "Built on request" |
29 | 244 | 248 | ||
30 | 249 | @property | ||
31 | 250 | def build_args(self): | ||
32 | 251 | return "\n".join( | ||
33 | 252 | "%s=%s" % (k, v) | ||
34 | 253 | for k, v in sorted(self.context.build_args.items())) | ||
35 | 254 | |||
36 | 245 | 255 | ||
37 | 246 | def builds_for_recipe(recipe): | 256 | def builds_for_recipe(recipe): |
38 | 247 | """A list of interesting builds. | 257 | """A list of interesting builds. |
39 | @@ -676,6 +686,7 @@ class IOCIRecipeEditSchema(Interface): | |||
40 | 676 | "description", | 686 | "description", |
41 | 677 | "git_ref", | 687 | "git_ref", |
42 | 678 | "build_file", | 688 | "build_file", |
43 | 689 | "build_args", | ||
44 | 679 | "build_path", | 690 | "build_path", |
45 | 680 | "build_daily", | 691 | "build_daily", |
46 | 681 | "require_virtualized", | 692 | "require_virtualized", |
47 | @@ -683,7 +694,45 @@ class IOCIRecipeEditSchema(Interface): | |||
48 | 683 | ]) | 694 | ]) |
49 | 684 | 695 | ||
50 | 685 | 696 | ||
52 | 686 | class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin): | 697 | class OCIRecipeFormMixin: |
53 | 698 | """Mixin with common processing for both edit and add views.""" | ||
54 | 699 | custom_widget_build_args = CustomWidgetFactory( | ||
55 | 700 | TextAreaWidget, height=5, width=100) | ||
56 | 701 | |||
57 | 702 | def createBuildArgsField(self): | ||
58 | 703 | """Create a form field for OCIRecipe.build_args attribute.""" | ||
59 | 704 | if IOCIRecipe.providedBy(self.context): | ||
60 | 705 | default = "\n".join( | ||
61 | 706 | "%s=%s" % (k, v) | ||
62 | 707 | for k, v in sorted(self.context.build_args.items())) | ||
63 | 708 | else: | ||
64 | 709 | default = "" | ||
65 | 710 | return FormFields(Text( | ||
66 | 711 | __name__='build_args', | ||
67 | 712 | title=u'Build-time ARG variables', | ||
68 | 713 | description=("One per line. Each ARG should be in the format " | ||
69 | 714 | "of ARG_KEY=arg_value."), | ||
70 | 715 | default=default, | ||
71 | 716 | required=False, readonly=False)) | ||
72 | 717 | |||
73 | 718 | def validateBuildArgs(self, data): | ||
74 | 719 | field_value = data.get('build_args') | ||
75 | 720 | if not field_value: | ||
76 | 721 | return | ||
77 | 722 | build_args = {} | ||
78 | 723 | for i, line in enumerate(field_value.split("\n")): | ||
79 | 724 | if '=' not in line: | ||
80 | 725 | msg = ("'%s' at line %s is not a valid KEY=value pair." % | ||
81 | 726 | (line, i + 1)) | ||
82 | 727 | self.setFieldError("build_args", six.text_type(msg)) | ||
83 | 728 | return | ||
84 | 729 | k, v = line.split('=', 1) | ||
85 | 730 | build_args[k] = v | ||
86 | 731 | data['build_args'] = build_args | ||
87 | 732 | |||
88 | 733 | |||
89 | 734 | class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin, | ||
90 | 735 | OCIRecipeFormMixin): | ||
91 | 687 | """View for creating OCI recipes.""" | 736 | """View for creating OCI recipes.""" |
92 | 688 | 737 | ||
93 | 689 | page_title = label = "Create a new OCI recipe" | 738 | page_title = label = "Create a new OCI recipe" |
94 | @@ -708,6 +757,7 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin): | |||
95 | 708 | def setUpFields(self): | 757 | def setUpFields(self): |
96 | 709 | """See `LaunchpadFormView`.""" | 758 | """See `LaunchpadFormView`.""" |
97 | 710 | super(OCIRecipeAddView, self).setUpFields() | 759 | super(OCIRecipeAddView, self).setUpFields() |
98 | 760 | self.form_fields += self.createBuildArgsField() | ||
99 | 711 | self.form_fields += self.createEnabledProcessors( | 761 | self.form_fields += self.createEnabledProcessors( |
100 | 712 | getUtility(IProcessorSet).getAll(), | 762 | getUtility(IProcessorSet).getAll(), |
101 | 713 | "The architectures that this OCI recipe builds for. Some " | 763 | "The architectures that this OCI recipe builds for. Some " |
102 | @@ -748,15 +798,16 @@ class OCIRecipeAddView(LaunchpadFormView, EnableProcessorsMixin): | |||
103 | 748 | "There is already an OCI recipe owned by %s in %s with " | 798 | "There is already an OCI recipe owned by %s in %s with " |
104 | 749 | "this name." % ( | 799 | "this name." % ( |
105 | 750 | owner.display_name, self.context.display_name)) | 800 | owner.display_name, self.context.display_name)) |
106 | 801 | self.validateBuildArgs(data) | ||
107 | 751 | 802 | ||
108 | 752 | @action("Create OCI recipe", name="create") | 803 | @action("Create OCI recipe", name="create") |
109 | 753 | def create_action(self, action, data): | 804 | def create_action(self, action, data): |
110 | 754 | recipe = getUtility(IOCIRecipeSet).new( | 805 | recipe = getUtility(IOCIRecipeSet).new( |
111 | 755 | name=data["name"], registrant=self.user, owner=data["owner"], | 806 | name=data["name"], registrant=self.user, owner=data["owner"], |
112 | 756 | oci_project=self.context, git_ref=data["git_ref"], | 807 | oci_project=self.context, git_ref=data["git_ref"], |
116 | 757 | build_file=data["build_file"], build_path=data["build_path"], | 808 | build_file=data["build_file"], description=data["description"], |
117 | 758 | description=data["description"], | 809 | build_daily=data["build_daily"], build_args=data["build_args"], |
118 | 759 | build_daily=data["build_daily"], processors=data["processors"]) | 810 | build_path=data["build_path"], processors=data["processors"]) |
119 | 760 | self.next_url = canonical_url(recipe) | 811 | self.next_url = canonical_url(recipe) |
120 | 761 | 812 | ||
121 | 762 | 813 | ||
122 | @@ -798,7 +849,8 @@ class OCIRecipeAdminView(BaseOCIRecipeEditView): | |||
123 | 798 | field_names = ("require_virtualized", "allow_internet") | 849 | field_names = ("require_virtualized", "allow_internet") |
124 | 799 | 850 | ||
125 | 800 | 851 | ||
127 | 801 | class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin): | 852 | class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin, |
128 | 853 | OCIRecipeFormMixin): | ||
129 | 802 | """View for editing OCI recipes.""" | 854 | """View for editing OCI recipes.""" |
130 | 803 | 855 | ||
131 | 804 | @property | 856 | @property |
132 | @@ -821,6 +873,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin): | |||
133 | 821 | def setUpFields(self): | 873 | def setUpFields(self): |
134 | 822 | """See `LaunchpadFormView`.""" | 874 | """See `LaunchpadFormView`.""" |
135 | 823 | super(OCIRecipeEditView, self).setUpFields() | 875 | super(OCIRecipeEditView, self).setUpFields() |
136 | 876 | self.form_fields += self.createBuildArgsField() | ||
137 | 824 | self.form_fields += self.createEnabledProcessors( | 877 | self.form_fields += self.createEnabledProcessors( |
138 | 825 | self.context.available_processors, | 878 | self.context.available_processors, |
139 | 826 | "The architectures that this OCI recipe builds for. Some " | 879 | "The architectures that this OCI recipe builds for. Some " |
140 | @@ -860,7 +913,7 @@ class OCIRecipeEditView(BaseOCIRecipeEditView, EnableProcessorsMixin): | |||
141 | 860 | # This processor is restricted and currently | 913 | # This processor is restricted and currently |
142 | 861 | # enabled. Leave it untouched. | 914 | # enabled. Leave it untouched. |
143 | 862 | data["processors"].append(processor) | 915 | data["processors"].append(processor) |
145 | 863 | 916 | self.validateBuildArgs(data) | |
146 | 864 | 917 | ||
147 | 865 | class OCIRecipeDeleteView(BaseOCIRecipeEditView): | 918 | class OCIRecipeDeleteView(BaseOCIRecipeEditView): |
148 | 866 | """View for deleting OCI recipes.""" | 919 | """View for deleting OCI recipes.""" |
149 | diff --git a/lib/lp/oci/browser/tests/test_ocirecipe.py b/lib/lp/oci/browser/tests/test_ocirecipe.py | |||
150 | index c854d14..9a3f0ea 100644 | |||
151 | --- a/lib/lp/oci/browser/tests/test_ocirecipe.py | |||
152 | +++ b/lib/lp/oci/browser/tests/test_ocirecipe.py | |||
153 | @@ -215,6 +215,26 @@ class TestOCIRecipeAddView(BaseTestOCIRecipeView): | |||
154 | 215 | "Build schedule:\nBuilt on request\nEdit OCI recipe\n", | 215 | "Build schedule:\nBuilt on request\nEdit OCI recipe\n", |
155 | 216 | MatchesTagText(content, "build-schedule")) | 216 | MatchesTagText(content, "build-schedule")) |
156 | 217 | 217 | ||
157 | 218 | def test_create_new_recipe_with_build_args(self): | ||
158 | 219 | oci_project = self.factory.makeOCIProject() | ||
159 | 220 | [git_ref] = self.factory.makeGitRefs() | ||
160 | 221 | browser = self.getViewBrowser( | ||
161 | 222 | oci_project, view_name="+new-recipe", user=self.person) | ||
162 | 223 | browser.getControl(name="field.name").value = "recipe-name" | ||
163 | 224 | browser.getControl("Description").value = "Recipe description" | ||
164 | 225 | browser.getControl("Git repository").value = ( | ||
165 | 226 | git_ref.repository.identity) | ||
166 | 227 | browser.getControl("Git branch").value = git_ref.path | ||
167 | 228 | browser.getControl("Build-time ARG variables").value = ( | ||
168 | 229 | "VAR1=10\nVAR2=20") | ||
169 | 230 | browser.getControl("Create OCI recipe").click() | ||
170 | 231 | |||
171 | 232 | content = find_main_content(browser.contents) | ||
172 | 233 | self.assertEqual("recipe-name", extract_text(content.h1)) | ||
173 | 234 | self.assertThat( | ||
174 | 235 | "Build-time\nARG variables:\nVAR1=10\nVAR2=20", | ||
175 | 236 | MatchesTagText(content, "build-args")) | ||
176 | 237 | |||
177 | 218 | def test_create_new_recipe_users_teams_as_owner_options(self): | 238 | def test_create_new_recipe_users_teams_as_owner_options(self): |
178 | 219 | # Teams that the user is in are options for the OCI recipe owner. | 239 | # Teams that the user is in are options for the OCI recipe owner. |
179 | 220 | self.factory.makeTeam( | 240 | self.factory.makeTeam( |
180 | @@ -466,6 +486,47 @@ class TestOCIRecipeEditView(OCIConfigHelperMixin, BaseTestOCIRecipeView): | |||
181 | 466 | login_person(self.person) | 486 | login_person(self.person) |
182 | 467 | self.assertRecipeProcessors(recipe, ["386", "amd64"]) | 487 | self.assertRecipeProcessors(recipe, ["386", "amd64"]) |
183 | 468 | 488 | ||
184 | 489 | def test_edit_build_args(self): | ||
185 | 490 | self.setUpDistroSeries() | ||
186 | 491 | oci_project = self.factory.makeOCIProject(pillar=self.distribution) | ||
187 | 492 | recipe = self.factory.makeOCIRecipe( | ||
188 | 493 | registrant=self.person, owner=self.person, | ||
189 | 494 | oci_project=oci_project, build_args={"VAR1": "xxx", "VAR2": "uu"}) | ||
190 | 495 | browser = self.getViewBrowser( | ||
191 | 496 | recipe, view_name="+edit", user=recipe.owner) | ||
192 | 497 | args = browser.getControl(name="field.build_args") | ||
193 | 498 | self.assertContentEqual("VAR1=xxx\r\nVAR2=uu", args.value) | ||
194 | 499 | args.value = "VAR=aa\nANOTHER_VAR=bbb" | ||
195 | 500 | browser.getControl("Update OCI recipe").click() | ||
196 | 501 | login_person(self.person) | ||
197 | 502 | IStore(recipe).reload(recipe) | ||
198 | 503 | self.assertEqual( | ||
199 | 504 | {"VAR": "aa", "ANOTHER_VAR": "bbb"}, recipe.build_args) | ||
200 | 505 | |||
201 | 506 | def test_edit_build_args_invalid_content(self): | ||
202 | 507 | self.setUpDistroSeries() | ||
203 | 508 | oci_project = self.factory.makeOCIProject(pillar=self.distribution) | ||
204 | 509 | recipe = self.factory.makeOCIRecipe( | ||
205 | 510 | registrant=self.person, owner=self.person, | ||
206 | 511 | oci_project=oci_project, build_args={"VAR1": "xxx", "VAR2": "uu"}) | ||
207 | 512 | browser = self.getViewBrowser( | ||
208 | 513 | recipe, view_name="+edit", user=recipe.owner) | ||
209 | 514 | args = browser.getControl(name="field.build_args") | ||
210 | 515 | self.assertContentEqual("VAR1=xxx\r\nVAR2=uu", args.value) | ||
211 | 516 | args.value = "VAR=aa\nmessed up text" | ||
212 | 517 | browser.getControl("Update OCI recipe").click() | ||
213 | 518 | |||
214 | 519 | # Error message should be shown. | ||
215 | 520 | content = find_main_content(browser.contents) | ||
216 | 521 | self.assertIn( | ||
217 | 522 | "'messed up text' at line 2 is not a valid KEY=value pair.", | ||
218 | 523 | extract_text(content)) | ||
219 | 524 | |||
220 | 525 | # Assert that recipe still have the original build_args. | ||
221 | 526 | login_person(self.person) | ||
222 | 527 | IStore(recipe).reload(recipe) | ||
223 | 528 | self.assertEqual({"VAR1": "xxx", "VAR2": "uu"}, recipe.build_args) | ||
224 | 529 | |||
225 | 469 | def test_edit_with_invisible_processor(self): | 530 | def test_edit_with_invisible_processor(self): |
226 | 470 | # It's possible for existing recipes to have an enabled processor | 531 | # It's possible for existing recipes to have an enabled processor |
227 | 471 | # that's no longer usable with the current distroseries, which will | 532 | # that's no longer usable with the current distroseries, which will |
228 | @@ -705,6 +766,37 @@ class TestOCIRecipeView(BaseTestOCIRecipeView): | |||
229 | 705 | """ % (oci_project_name, oci_project_display), | 766 | """ % (oci_project_name, oci_project_display), |
230 | 706 | self.getMainText(build.recipe)) | 767 | self.getMainText(build.recipe)) |
231 | 707 | 768 | ||
232 | 769 | def test_index_with_build_args(self): | ||
233 | 770 | oci_project = self.factory.makeOCIProject( | ||
234 | 771 | pillar=self.distroseries.distribution) | ||
235 | 772 | oci_project_name = oci_project.name | ||
236 | 773 | oci_project_display = oci_project.display_name | ||
237 | 774 | [ref] = self.factory.makeGitRefs( | ||
238 | 775 | owner=self.person, target=self.person, name="recipe-repository", | ||
239 | 776 | paths=["refs/heads/master"]) | ||
240 | 777 | recipe = self.makeOCIRecipe( | ||
241 | 778 | oci_project=oci_project, git_ref=ref, build_file="Dockerfile", | ||
242 | 779 | build_args={"VAR1": "123", "VAR2": "XXX"}) | ||
243 | 780 | build = self.makeBuild( | ||
244 | 781 | recipe=recipe, status=BuildStatus.FULLYBUILT, | ||
245 | 782 | duration=timedelta(minutes=30)) | ||
246 | 783 | self.assertTextMatchesExpressionIgnoreWhitespace("""\ | ||
247 | 784 | %s OCI project | ||
248 | 785 | recipe-name | ||
249 | 786 | .* | ||
250 | 787 | OCI recipe information | ||
251 | 788 | Owner: Test Person | ||
252 | 789 | OCI project: %s | ||
253 | 790 | Source: ~test-person/\\+git/recipe-repository:master | ||
254 | 791 | Build file path: Dockerfile | ||
255 | 792 | Build schedule: Built on request | ||
256 | 793 | Build-time\nARG variables: VAR1=123 VAR2=XXX | ||
257 | 794 | Latest builds | ||
258 | 795 | Status When complete Architecture | ||
259 | 796 | Successfully built 30 minutes ago 386 | ||
260 | 797 | """ % (oci_project_name, oci_project_display), | ||
261 | 798 | self.getMainText(build.recipe)) | ||
262 | 799 | |||
263 | 708 | def test_index_success_with_buildlog(self): | 800 | def test_index_success_with_buildlog(self): |
264 | 709 | # The build log is shown if it is there. | 801 | # The build log is shown if it is there. |
265 | 710 | build = self.makeBuild( | 802 | build = self.makeBuild( |
266 | diff --git a/lib/lp/oci/templates/ocirecipe-index.pt b/lib/lp/oci/templates/ocirecipe-index.pt | |||
267 | index c2ef69e..5c3ca56 100644 | |||
268 | --- a/lib/lp/oci/templates/ocirecipe-index.pt | |||
269 | +++ b/lib/lp/oci/templates/ocirecipe-index.pt | |||
270 | @@ -69,6 +69,18 @@ | |||
271 | 69 | <a tal:replace="structure view/menu:overview/edit/fmt:icon"/> | 69 | <a tal:replace="structure view/menu:overview/edit/fmt:icon"/> |
272 | 70 | </dd> | 70 | </dd> |
273 | 71 | </dl> | 71 | </dl> |
274 | 72 | <dl id="build-args" | ||
275 | 73 | tal:define="build_args view/build_args" | ||
276 | 74 | tal:condition="build_args"> | ||
277 | 75 | <dt> | ||
278 | 76 | Build-time | ||
279 | 77 | <a href="https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables---build-arg" | ||
280 | 78 | target="_blank">ARG variables</a>: | ||
281 | 79 | </dt> | ||
282 | 80 | <dd> | ||
283 | 81 | <pre tal:content="build_args" /> | ||
284 | 82 | </dd> | ||
285 | 83 | </dl> | ||
286 | 72 | </div> | 84 | </div> |
287 | 73 | 85 | ||
288 | 74 | <h2>Latest builds</h2> | 86 | <h2>Latest builds</h2> |
289 | diff --git a/lib/lp/oci/templates/ocirecipe-new.pt b/lib/lp/oci/templates/ocirecipe-new.pt | |||
290 | index 0bc156a..1cae71e 100644 | |||
291 | --- a/lib/lp/oci/templates/ocirecipe-new.pt | |||
292 | +++ b/lib/lp/oci/templates/ocirecipe-new.pt | |||
293 | @@ -35,6 +35,9 @@ | |||
294 | 35 | <tal:widget define="widget nocall:view/widgets/build_daily"> | 35 | <tal:widget define="widget nocall:view/widgets/build_daily"> |
295 | 36 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | 36 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
296 | 37 | </tal:widget> | 37 | </tal:widget> |
297 | 38 | <tal:widget define="widget nocall:view/widgets/build_args"> | ||
298 | 39 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | ||
299 | 40 | </tal:widget> | ||
300 | 38 | <tal:widget define="widget nocall:view/widgets/processors"> | 41 | <tal:widget define="widget nocall:view/widgets/processors"> |
301 | 39 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> | 42 | <metal:block use-macro="context/@@launchpad_form/widget_row" /> |
302 | 40 | </tal:widget> | 43 | </tal:widget> |
I forgot to add the screenshots for this. I'll do it in some minutes.