pgkiln 0.53.0 → 0.55.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.
- package/SECURITY.md +2 -0
- package/db/migrations/127_builder_language.sql +16 -0
- package/docs/apex-feature-parity.md +7 -7
- package/docs/guide/03-builder.md +62 -8
- package/docs/guide/12-development.md +50 -3
- package/examples/gallery/README.md +37 -6
- package/examples/gallery/gallery.json +23 -1
- package/examples/gallery/samples/sample-calendar.sql +172 -0
- package/examples/gallery/samples/sample-cards.sql +157 -0
- package/examples/gallery/samples/sample-charts.sql +265 -0
- package/examples/gallery/samples/sample-dynamic-actions.sql +177 -0
- package/examples/gallery/samples/sample-grids.sql +151 -0
- package/examples/gallery/samples/sample-reporting.sql +151 -0
- package/examples/gallery/samples/ux-patterns.sql +295 -0
- package/examples/gallery/utilities/announcements.json +24 -0
- package/examples/gallery/utilities/lookup-lists.json +28 -0
- package/examples/gallery/utilities/release-notes.json +41 -0
- package/package.json +1 -1
- package/public/app.css +2 -0
- package/public/builder.css +39 -1
- package/public/worksheet.css +3 -2
- package/src/builder/advisor.ts +1 -1
- package/src/builder/ai-agents.ts +5 -5
- package/src/builder/ai-builder.ts +3 -3
- package/src/builder/api.ts +1 -1
- package/src/builder/autorest.ts +2 -2
- package/src/builder/availability.ts +1 -1
- package/src/builder/backups.ts +1 -1
- package/src/builder/changelog.ts +1 -1
- package/src/builder/data-modeler.ts +4 -4
- package/src/builder/data-security.ts +1 -1
- package/src/builder/dataload.ts +1 -1
- package/src/builder/datapump.ts +1 -1
- package/src/builder/ddl-tools.ts +2 -2
- package/src/builder/designer.ts +134 -124
- package/src/builder/diagnostics.ts +2 -2
- package/src/builder/errors.ts +10 -12
- package/src/builder/feedback.ts +1 -1
- package/src/builder/gallery.ts +94 -47
- package/src/builder/globalization.ts +1 -1
- package/src/builder/home.ts +59 -57
- package/src/builder/i18n/de.ts +675 -0
- package/src/builder/i18n/en.ts +678 -0
- package/src/builder/i18n/es.ts +675 -0
- package/src/builder/i18n/fr.ts +675 -0
- package/src/builder/i18n/nl.ts +675 -0
- package/src/builder/i18n/pt.ts +675 -0
- package/src/builder/i18n.ts +116 -0
- package/src/builder/language.ts +74 -0
- package/src/builder/launchpad.ts +13 -8
- package/src/builder/object-browser.ts +1 -1
- package/src/builder/plugins.ts +1 -1
- package/src/builder/querybuilder.ts +1 -1
- package/src/builder/quicksql.ts +1 -1
- package/src/builder/rest-modules.ts +2 -2
- package/src/builder/routes.ts +88 -66
- package/src/builder/sampledata.ts +1 -1
- package/src/builder/scheduling.ts +1 -1
- package/src/builder/scripts.ts +1 -1
- package/src/builder/search.ts +1 -1
- package/src/builder/shared.ts +5 -5
- package/src/builder/sql-charts.ts +2 -2
- package/src/builder/sql-monitor.ts +2 -2
- package/src/builder/sql-tools.ts +6 -0
- package/src/builder/sql-utilities.ts +3 -3
- package/src/builder/sql.ts +24 -22
- package/src/builder/static-files.ts +1 -1
- package/src/builder/subscriptions.ts +1 -1
- package/src/builder/supporting.ts +1 -1
- package/src/builder/table-editor.ts +1 -1
- package/src/builder/team.ts +7 -5
- package/src/builder/themeroller.ts +1 -1
- package/src/builder/top-sql.ts +1 -1
- package/src/builder/ui.ts +87 -77
- package/src/builder/unload.ts +1 -1
- package/src/builder/wizards.ts +1 -1
- package/src/builder/workflow-migrate.ts +1 -1
- package/src/builder/workingcopies.ts +2 -2
- package/src/builder/worksheet.ts +1 -1
- package/src/builder/workspace-home.ts +62 -59
- package/src/builder/workspaces.ts +2 -1
- package/src/runtime/charts.ts +4 -1
- package/src/runtime/regions.ts +8 -5
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- Sample app "Sample Calendar" (App Gallery → Sample Apps)
|
|
3
|
+
--
|
|
4
|
+
-- The calendar region: month, week, day and list views, all-day and timed
|
|
5
|
+
-- events, events over several days, create on click, drag and drop through
|
|
6
|
+
-- a PL/pgSQL function with its own rule, and a calendar filtered by an item.
|
|
7
|
+
-- The events are placed around today, so there is always something to see.
|
|
8
|
+
--
|
|
9
|
+
-- Installed by an administrator from the App Gallery, or with
|
|
10
|
+
-- psql "$DATABASE_URL" -1 -f examples/gallery/samples/sample-calendar.sql
|
|
11
|
+
-- then open /a/sample-calendar and sign in as demo / demo.
|
|
12
|
+
-- =====================================================================
|
|
13
|
+
|
|
14
|
+
create or replace function pg_temp.page_id(p_alias text, p_page int) returns int language sql stable as
|
|
15
|
+
$$ select p.id from meta.page p join meta.app a on a.id = p.app_id where a.alias = p_alias and p.page_no = p_page $$;
|
|
16
|
+
|
|
17
|
+
-- 1. Schema and data ---------------------------------------------------------
|
|
18
|
+
create schema sample_calendar;
|
|
19
|
+
|
|
20
|
+
create table sample_calendar.event (
|
|
21
|
+
id int generated always as identity primary key,
|
|
22
|
+
title text not null,
|
|
23
|
+
category text not null default 'Meeting' check (category in ('Meeting', 'Training', 'Travel', 'Holiday')),
|
|
24
|
+
starts timestamp not null,
|
|
25
|
+
ends timestamp,
|
|
26
|
+
location text,
|
|
27
|
+
notes text,
|
|
28
|
+
check (ends is null or ends >= starts)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
-- this week's Monday as the anchor: meetings by the hour, all-day and multi-day events
|
|
32
|
+
insert into sample_calendar.event (title, category, starts, ends, location)
|
|
33
|
+
select e.title, e.category, w.monday + e.day_offset + e.start_at, case when e.end_at is null then null else w.monday + e.end_day + e.end_at end, e.location
|
|
34
|
+
from (select date_trunc('week', current_date)::date as monday) w,
|
|
35
|
+
(values
|
|
36
|
+
('Team stand-up', 'Meeting', 0, interval '9 hours', 0, interval '9 hours 15 minutes', 'Room 1'),
|
|
37
|
+
('Planning', 'Meeting', 0, interval '10 hours', 0, interval '11 hours 30 minutes', 'Room 2'),
|
|
38
|
+
('Customer call', 'Meeting', 1, interval '14 hours', 1, interval '15 hours', 'Online'),
|
|
39
|
+
('Team stand-up', 'Meeting', 2, interval '9 hours', 2, interval '9 hours 15 minutes', 'Room 1'),
|
|
40
|
+
('SQL course', 'Training', 2, interval '0 hours', 3, interval '0 hours', 'Training centre'),
|
|
41
|
+
('Trip to the trade fair', 'Travel', 7, interval '0 hours', 9, interval '0 hours', 'Utrecht'),
|
|
42
|
+
('Review', 'Meeting', 4, interval '13 hours', 4, interval '14 hours', 'Room 2'),
|
|
43
|
+
('Lunch and learn', 'Training', 8, interval '12 hours', 8, interval '13 hours', 'Canteen'),
|
|
44
|
+
('Retrospective', 'Meeting', 11, interval '15 hours', 11, interval '16 hours', 'Room 1'),
|
|
45
|
+
('Public holiday', 'Holiday', 14, interval '0 hours', null, null, null),
|
|
46
|
+
('Budget meeting', 'Meeting', -3, interval '10 hours', -3, interval '12 hours', 'Board room'),
|
|
47
|
+
('First aid course', 'Training', -7, interval '0 hours', -6, interval '0 hours', 'Training centre'),
|
|
48
|
+
('Quarter review', 'Meeting', 18, interval '9 hours', 18, interval '11 hours', 'Board room'),
|
|
49
|
+
('Holiday', 'Holiday', 21, interval '0 hours', 25, interval '0 hours', null)
|
|
50
|
+
) as e (title, category, day_offset, start_at, end_day, end_at, location);
|
|
51
|
+
|
|
52
|
+
-- drag and drop: the calendar calls this with the new start and end; holidays stay where they are
|
|
53
|
+
create function sample_calendar.move_event(p_id int, p_start timestamp, p_end timestamp) returns void
|
|
54
|
+
language plpgsql as $$
|
|
55
|
+
begin
|
|
56
|
+
if exists (select 1 from sample_calendar.event where id = p_id and category = 'Holiday') then
|
|
57
|
+
raise exception 'Holidays can''t be moved.';
|
|
58
|
+
end if;
|
|
59
|
+
update sample_calendar.event set starts = p_start, ends = p_end where id = p_id;
|
|
60
|
+
end
|
|
61
|
+
$$;
|
|
62
|
+
|
|
63
|
+
-- 2. The application's database role ----------------------------------------
|
|
64
|
+
do $$ begin
|
|
65
|
+
if not exists (select 1 from pg_roles where rolname = 'app_sample_calendar') then create role app_sample_calendar nologin; end if;
|
|
66
|
+
end $$;
|
|
67
|
+
grant app_sample_calendar to pgkiln_runtime;
|
|
68
|
+
grant usage on schema sample_calendar to app_sample_calendar;
|
|
69
|
+
grant select, insert, update, delete on all tables in schema sample_calendar to app_sample_calendar;
|
|
70
|
+
grant execute on all functions in schema sample_calendar to app_sample_calendar;
|
|
71
|
+
|
|
72
|
+
-- 3. The application ----------------------------------------------------------
|
|
73
|
+
insert into meta.app (alias, name, home_page, authentication, db_role)
|
|
74
|
+
values ('sample-calendar', 'Sample Calendar', 1, 'app_users', 'app_sample_calendar');
|
|
75
|
+
|
|
76
|
+
insert into meta.app_user (app_id, username, password_hash, roles)
|
|
77
|
+
select id, 'demo', meta.hash_password('demo'), '{}' from meta.app where alias = 'sample-calendar';
|
|
78
|
+
|
|
79
|
+
insert into meta.page (app_id, page_no, name, title, parent_page)
|
|
80
|
+
select a.id, p.no, p.name, p.name, p.parent
|
|
81
|
+
from meta.app a, (values
|
|
82
|
+
(1, 'Sample Calendar', null), (2, 'Month', 1), (4, 'Week planner', 1), (5, 'Agenda', 1), (6, 'By category', 1)
|
|
83
|
+
) as p (no, name, parent)
|
|
84
|
+
where a.alias = 'sample-calendar';
|
|
85
|
+
|
|
86
|
+
-- page 3: the event form, a modal dialog (the Form wizard), opened from the calendars
|
|
87
|
+
select meta.generate_page('sample-calendar', 'form', 'sample_calendar.event', 3,
|
|
88
|
+
'{"label": "Event", "mode": "modal", "return_page": 2, "nav": false}');
|
|
89
|
+
update meta.page set parent_page = 2 where id = pg_temp.page_id('sample-calendar', 3);
|
|
90
|
+
update meta.item set type = 'select', lov = 'STATIC:Meeting,Training,Travel,Holiday'
|
|
91
|
+
where page_id = pg_temp.page_id('sample-calendar', 3) and name = 'P3_CATEGORY';
|
|
92
|
+
update meta.item set help = 'Leave it empty for an event without an end (all day when the start is at midnight).'
|
|
93
|
+
where page_id = pg_temp.page_id('sample-calendar', 3) and name = 'P3_ENDS';
|
|
94
|
+
|
|
95
|
+
insert into meta.nav_entry (app_id, seq, label, icon, target_page)
|
|
96
|
+
select a.id, n.seq, n.label, n.icon, n.page
|
|
97
|
+
from meta.app a, (values
|
|
98
|
+
(10, 'Home', 'home', 1), (20, 'Month', 'calendar', 2), (30, 'Week planner', 'clock', 4),
|
|
99
|
+
(40, 'Agenda', 'list', 5), (50, 'By category', 'filter', 6)
|
|
100
|
+
) as n (seq, label, icon, page)
|
|
101
|
+
where a.alias = 'sample-calendar';
|
|
102
|
+
|
|
103
|
+
insert into meta.list (app_id, name, type, description)
|
|
104
|
+
select id, 'CALENDAR_TOPICS', 'static', 'The pages of Sample Calendar' from meta.app where alias = 'sample-calendar';
|
|
105
|
+
|
|
106
|
+
insert into meta.list_entry (app_id, list_name, seq, label, icon, target_page, description)
|
|
107
|
+
select a.id, 'CALENDAR_TOPICS', e.seq, e.label, e.icon, e.page, e.description
|
|
108
|
+
from meta.app a, (values
|
|
109
|
+
(10, 'Month', 'calendar', 2, 'All four views; click a day to add an event, drag an event to move it.'),
|
|
110
|
+
(20, 'Week planner', 'clock', 4, 'Week and day views from 7:00 to 19:00, opened on the week.'),
|
|
111
|
+
(30, 'Agenda', 'list', 5, 'The list view only: a month of events per day.'),
|
|
112
|
+
(40, 'By category', 'filter', 6, 'A calendar whose query uses a page item, refreshed when it changes.')
|
|
113
|
+
) as e (seq, label, icon, page, description)
|
|
114
|
+
where a.alias = 'sample-calendar';
|
|
115
|
+
|
|
116
|
+
insert into meta.region (page_id, seq, title, type, columns, template, source, config, static_id)
|
|
117
|
+
select pg_temp.page_id('sample-calendar', r.page), r.seq, r.title, r.type, r.cols, r.template, r.source, r.config::jsonb, r.sid
|
|
118
|
+
from (values
|
|
119
|
+
-- 1 Home
|
|
120
|
+
(1, 10, 'Sample Calendar', 'static', 12, 'hero',
|
|
121
|
+
'<p>A calendar region shows the rows of a SELECT with a <code>start_date</code>, an optional <code>end_date</code> and a <code>title</code>. Dates are all-day events, timestamps events with a time. Every view is a plain link, so the calendar works without JavaScript and can be bookmarked.</p>',
|
|
122
|
+
'{"icon": "calendar"}', null),
|
|
123
|
+
(1, 20, 'Topics', 'list', 12, 'plain', null, '{"list": "CALENDAR_TOPICS", "template": "cards"}', null),
|
|
124
|
+
(1, 30, 'Today and tomorrow', 'calendar', 12, 'standard',
|
|
125
|
+
E'select starts as start_date, ends as end_date, title, id\n from sample_calendar.event\n where starts < current_date + 2 and coalesce(ends, starts) >= current_date',
|
|
126
|
+
'{"views": ["list"], "link": {"page": 3, "items": {"P3_ID": "#id#"}}}', null),
|
|
127
|
+
|
|
128
|
+
-- 2 Month
|
|
129
|
+
(2, 5, 'About this page', 'static', 12, 'plain',
|
|
130
|
+
'<p class="lead">The attribute <code>link</code> opens an event in the form; <code>create</code> adds a <strong>+</strong> to every day and hour that opens the form with <code>#start#</code> and <code>#end#</code> filled in; <code>move</code> lets you drag events to another day or hour. The move runs <code>sample_calendar.move_event</code>, which refuses to move a holiday: try it.</p>',
|
|
131
|
+
'{}', null),
|
|
132
|
+
(2, 10, 'Events', 'calendar', 12, 'standard',
|
|
133
|
+
E'select starts as start_date, ends as end_date, title || coalesce('' ('' || location || '')'', '''') as title, id\n from sample_calendar.event',
|
|
134
|
+
'{"views": ["month", "week", "day", "list"], "link": {"page": 3, "items": {"P3_ID": "#id#"}},
|
|
135
|
+
"create": {"page": 3, "items": {"P3_STARTS": "#start#", "P3_ENDS": "#end#"}},
|
|
136
|
+
"move": "select sample_calendar.move_event(:EVENT_ID::int, :NEW_START::timestamp, :NEW_END::timestamp)"}', 'events'),
|
|
137
|
+
|
|
138
|
+
-- 4 Week planner
|
|
139
|
+
(4, 5, 'About this page', 'static', 12, 'plain',
|
|
140
|
+
'<p class="lead">With <code>"views": ["week", "day"]</code> and <code>"view": "week"</code> the calendar opens on the week; <code>day_start</code> and <code>day_end</code> set its hours (widened when an event needs it). On a phone the week becomes an agenda list.</p>',
|
|
141
|
+
'{}', null),
|
|
142
|
+
(4, 10, 'This week', 'calendar', 12, 'standard',
|
|
143
|
+
E'select starts as start_date, ends as end_date, title, id\n from sample_calendar.event where category in (''Meeting'', ''Training'')',
|
|
144
|
+
'{"views": ["week", "day"], "view": "week", "day_start": 7, "day_end": 19, "link": {"page": 3, "items": {"P3_ID": "#id#"}},
|
|
145
|
+
"create": {"page": 3, "items": {"P3_STARTS": "#start#", "P3_ENDS": "#end#"}}}', null),
|
|
146
|
+
|
|
147
|
+
-- 5 Agenda
|
|
148
|
+
(5, 5, 'About this page', 'static', 12, 'plain',
|
|
149
|
+
'<p class="lead">The <code>list</code> view alone: the month''s events per day, with ‹ and › to move by a month.</p>',
|
|
150
|
+
'{}', null),
|
|
151
|
+
(5, 10, 'Agenda', 'calendar', 12, 'standard',
|
|
152
|
+
E'select starts as start_date, ends as end_date, category || '': '' || title as title, id\n from sample_calendar.event',
|
|
153
|
+
'{"views": ["list"], "link": {"page": 3, "items": {"P3_ID": "#id#"}}}', null),
|
|
154
|
+
|
|
155
|
+
-- 6 By category
|
|
156
|
+
(6, 5, 'About this page', 'static', 12, 'plain',
|
|
157
|
+
'<p class="lead">The query uses the page item <code>:P6_CATEGORY</code>. A dynamic action refreshes the calendar when the item changes (without JavaScript, the item submits the page).</p>',
|
|
158
|
+
'{}', null),
|
|
159
|
+
(6, 10, 'Filter', 'static', 12, 'standard', null, '{}', 'filter'),
|
|
160
|
+
(6, 20, 'Events of the category', 'calendar', 12, 'standard',
|
|
161
|
+
E'select starts as start_date, ends as end_date, title, id\n from sample_calendar.event\n where :P6_CATEGORY is null or category = :P6_CATEGORY',
|
|
162
|
+
'{"views": ["month", "list"], "link": {"page": 3, "items": {"P3_ID": "#id#"}}}', 'category-calendar')
|
|
163
|
+
) as r (page, seq, title, type, cols, template, source, config, sid);
|
|
164
|
+
|
|
165
|
+
insert into meta.item (page_id, region_id, seq, name, label, type, lov, config)
|
|
166
|
+
select pg_temp.page_id('sample-calendar', 6), r.id, 10, 'P6_CATEGORY', 'Category', 'radio',
|
|
167
|
+
'STATIC:Meeting,Training,Travel,Holiday', '{"warn_unsaved": false}'
|
|
168
|
+
from meta.region r where r.page_id = pg_temp.page_id('sample-calendar', 6) and r.static_id = 'filter';
|
|
169
|
+
|
|
170
|
+
insert into meta.dynamic_action (page_id, seq, name, event, trigger_element, action, affected_region_id, items_to_submit)
|
|
171
|
+
select pg_temp.page_id('sample-calendar', 6), 10, 'Refresh the calendar', 'change', 'P6_CATEGORY', 'refresh_region', r.id, 'P6_CATEGORY'
|
|
172
|
+
from meta.region r where r.page_id = pg_temp.page_id('sample-calendar', 6) and r.static_id = 'category-calendar';
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- Sample app "Sample Cards" (App Gallery → Sample Apps)
|
|
3
|
+
--
|
|
4
|
+
-- Cards in all the forms pgkiln has: a cards region with title, subtitle,
|
|
5
|
+
-- body, badge and icon that opens a form; KPI tiles (style metric); a list
|
|
6
|
+
-- region as cards, badges, links and tabs; and the built-in template
|
|
7
|
+
-- components (media list, metric cards, badges, avatars) as cards.
|
|
8
|
+
--
|
|
9
|
+
-- Installed by an administrator from the App Gallery, or with
|
|
10
|
+
-- psql "$DATABASE_URL" -1 -f examples/gallery/samples/sample-cards.sql
|
|
11
|
+
-- then open /a/sample-cards and sign in as demo / demo.
|
|
12
|
+
-- =====================================================================
|
|
13
|
+
|
|
14
|
+
create or replace function pg_temp.page_id(p_alias text, p_page int) returns int language sql stable as
|
|
15
|
+
$$ select p.id from meta.page p join meta.app a on a.id = p.app_id where a.alias = p_alias and p.page_no = p_page $$;
|
|
16
|
+
|
|
17
|
+
-- 1. Schema and data ---------------------------------------------------------
|
|
18
|
+
create schema sample_cards;
|
|
19
|
+
|
|
20
|
+
create table sample_cards.product (
|
|
21
|
+
id int generated always as identity primary key,
|
|
22
|
+
name text not null unique,
|
|
23
|
+
category text not null check (category in ('Furniture', 'Lighting', 'Accessories', 'Plants')),
|
|
24
|
+
price numeric(10, 2) not null check (price >= 0),
|
|
25
|
+
stock int not null default 0 check (stock >= 0),
|
|
26
|
+
rating int check (rating between 1 and 5),
|
|
27
|
+
icon text not null default 'box',
|
|
28
|
+
description text
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
insert into sample_cards.product (name, category, price, stock, rating, icon, description) values
|
|
32
|
+
('Oak desk', 'Furniture', 640, 12, 5, 'briefcase', 'A solid oak desk, 160 by 80 cm, with a cable tray.'),
|
|
33
|
+
('Task chair', 'Furniture', 290, 3, 4, 'user', 'Adjustable seat, arms and lumbar support.'),
|
|
34
|
+
('Bookcase', 'Furniture', 210, 0, 4, 'book', 'Five shelves of birch plywood.'),
|
|
35
|
+
('Desk lamp', 'Lighting', 79, 25, 5, 'lightbulb', 'Warm LED light with a dimmer.'),
|
|
36
|
+
('Floor lamp', 'Lighting', 149, 7, 3, 'sun', 'A reading light that bends where you need it.'),
|
|
37
|
+
('Monitor arm', 'Accessories', 95, 40, 4, 'tool', 'Holds one screen up to 32 inches.'),
|
|
38
|
+
('Desk mat', 'Accessories', 29, 60, 4, 'layers', 'Felt, 90 by 40 cm.'),
|
|
39
|
+
('Cable box', 'Accessories', 24, 2, 3, 'box', 'Hides a power strip and its cables.'),
|
|
40
|
+
('Fiddle-leaf fig', 'Plants', 59, 9, 5, 'leaf', 'Large leaves; likes bright, indirect light.'),
|
|
41
|
+
('Snake plant', 'Plants', 35, 18, 4, 'leaf', 'Thrives on neglect.');
|
|
42
|
+
|
|
43
|
+
-- 2. The application's database role ----------------------------------------
|
|
44
|
+
do $$ begin
|
|
45
|
+
if not exists (select 1 from pg_roles where rolname = 'app_sample_cards') then create role app_sample_cards nologin; end if;
|
|
46
|
+
end $$;
|
|
47
|
+
grant app_sample_cards to pgkiln_runtime;
|
|
48
|
+
grant usage on schema sample_cards to app_sample_cards;
|
|
49
|
+
grant select, insert, update, delete on all tables in schema sample_cards to app_sample_cards;
|
|
50
|
+
|
|
51
|
+
-- 3. The application ----------------------------------------------------------
|
|
52
|
+
insert into meta.app (alias, name, home_page, authentication, db_role)
|
|
53
|
+
values ('sample-cards', 'Sample Cards', 1, 'app_users', 'app_sample_cards');
|
|
54
|
+
|
|
55
|
+
insert into meta.app_user (app_id, username, password_hash, roles)
|
|
56
|
+
select id, 'demo', meta.hash_password('demo'), '{}' from meta.app where alias = 'sample-cards';
|
|
57
|
+
|
|
58
|
+
insert into meta.page (app_id, page_no, name, title, parent_page)
|
|
59
|
+
select a.id, p.no, p.name, p.name, p.parent
|
|
60
|
+
from meta.app a, (values
|
|
61
|
+
(1, 'Sample Cards', null), (2, 'Cards', 1), (4, 'KPI tiles', 1), (5, 'Lists as cards', 1), (6, 'Template components', 1)
|
|
62
|
+
) as p (no, name, parent)
|
|
63
|
+
where a.alias = 'sample-cards';
|
|
64
|
+
|
|
65
|
+
-- page 3: the product form, a modal dialog opened from the cards (the Form wizard)
|
|
66
|
+
select meta.generate_page('sample-cards', 'form', 'sample_cards.product', 3,
|
|
67
|
+
'{"label": "Product", "mode": "modal", "return_page": 2, "nav": false}');
|
|
68
|
+
update meta.page set parent_page = 2 where id = pg_temp.page_id('sample-cards', 3);
|
|
69
|
+
update meta.item set type = 'select', lov = 'STATIC:Furniture,Lighting,Accessories,Plants'
|
|
70
|
+
where page_id = pg_temp.page_id('sample-cards', 3) and name = 'P3_CATEGORY';
|
|
71
|
+
update meta.item set type = 'rating'
|
|
72
|
+
where page_id = pg_temp.page_id('sample-cards', 3) and name = 'P3_RATING';
|
|
73
|
+
|
|
74
|
+
insert into meta.nav_entry (app_id, seq, label, icon, target_page)
|
|
75
|
+
select a.id, n.seq, n.label, n.icon, n.page
|
|
76
|
+
from meta.app a, (values
|
|
77
|
+
(10, 'Home', 'home', 1), (20, 'Cards', 'layers', 2), (30, 'KPI tiles', 'dashboard', 4),
|
|
78
|
+
(40, 'Lists as cards', 'list', 5), (50, 'Template components', 'grid', 6)
|
|
79
|
+
) as n (seq, label, icon, page)
|
|
80
|
+
where a.alias = 'sample-cards';
|
|
81
|
+
|
|
82
|
+
insert into meta.list (app_id, name, type, description, query)
|
|
83
|
+
select a.id, l.name, l.type, l.description, l.query
|
|
84
|
+
from meta.app a, (values
|
|
85
|
+
('CARD_TOPICS', 'static', 'The pages of Sample Cards', null),
|
|
86
|
+
('CATEGORIES', 'sql', 'The categories with the number of products, from a query',
|
|
87
|
+
E'select category as label, 2 as page, null::jsonb as items,\n case category when ''Furniture'' then ''briefcase'' when ''Lighting'' then ''lightbulb'' when ''Plants'' then ''leaf'' else ''box'' end as icon,\n count(*)::text as badge, count(*) filter (where stock = 0) || '' out of stock'' as description\n from sample_cards.product group by category order by category')
|
|
88
|
+
) as l (name, type, description, query)
|
|
89
|
+
where a.alias = 'sample-cards';
|
|
90
|
+
|
|
91
|
+
insert into meta.list_entry (app_id, list_name, seq, label, icon, target_page, description)
|
|
92
|
+
select a.id, 'CARD_TOPICS', e.seq, e.label, e.icon, e.page, e.description
|
|
93
|
+
from meta.app a, (values
|
|
94
|
+
(10, 'Cards', 'layers', 2, 'A cards region: title, subtitle, body, badge and icon from the query, each card a link to the form.'),
|
|
95
|
+
(20, 'KPI tiles', 'dashboard', 4, 'Key figures as tiles: the style metric, a format mask and an empty text.'),
|
|
96
|
+
(30, 'Lists as cards', 'list', 5, 'A list (Shared Components) shown as cards, badges, links or tabs.'),
|
|
97
|
+
(40, 'Template components', 'grid', 6, 'Media list, metric cards, badges and avatars: the built-in template components.')
|
|
98
|
+
) as e (seq, label, icon, page, description)
|
|
99
|
+
where a.alias = 'sample-cards';
|
|
100
|
+
|
|
101
|
+
insert into meta.region (page_id, seq, title, type, columns, template, source, config)
|
|
102
|
+
select pg_temp.page_id('sample-cards', r.page), r.seq, r.title, r.type, r.cols, r.template, r.source, r.config::jsonb
|
|
103
|
+
from (values
|
|
104
|
+
-- 1 Home
|
|
105
|
+
(1, 10, 'Sample Cards', 'static', 12, 'hero',
|
|
106
|
+
'<p>Cards show rows as tiles instead of table rows: good for a few dozen things with a name, a picture or icon and a key figure. pgkiln has a cards region, KPI tiles, lists shown as cards and template components; this application shows each on a page of its own.</p>',
|
|
107
|
+
'{"icon": "layers"}'),
|
|
108
|
+
(1, 20, 'Topics', 'list', 12, 'plain', null, '{"list": "CARD_TOPICS", "template": "cards"}'),
|
|
109
|
+
|
|
110
|
+
-- 2 Cards
|
|
111
|
+
(2, 5, 'About this page', 'static', 12, 'plain',
|
|
112
|
+
'<p class="lead">A <strong>cards</strong> region reads the columns <code>title</code>, <code>subtitle</code>, <code>body</code>, <code>badge</code> and <code>icon</code> of its query; other columns can be used in its link. Here every card opens the product form as a dialog (the attribute <code>link</code>), and the badge shows the price with a format mask.</p>',
|
|
113
|
+
'{}'),
|
|
114
|
+
(2, 10, 'Products', 'cards', 12, 'plain',
|
|
115
|
+
E'select name as title, category || '' · '' || stock || '' in stock'' as subtitle, description as body,\n price as badge, icon, id\n from sample_cards.product order by category, name',
|
|
116
|
+
'{"link": {"page": 3, "items": {"P3_ID": "#id#"}}, "formats": {"badge": "FML999G990D00"}, "empty": "No products yet."}'),
|
|
117
|
+
|
|
118
|
+
-- 4 KPI tiles
|
|
119
|
+
(4, 5, 'About this page', 'static', 12, 'plain',
|
|
120
|
+
'<p class="lead">With <code>{"style": "metric"}</code> a cards region shows KPI tiles: the <code>badge</code> as a big value and the <code>title</code> as its label. A query with no rows shows the region''s <code>empty</code> text instead.</p>',
|
|
121
|
+
'{}'),
|
|
122
|
+
(4, 10, 'Key figures', 'cards', 12, 'plain',
|
|
123
|
+
E'select ''Products'' as title, count(*)::text as badge, ''box'' as icon from sample_cards.product\nunion all\nselect ''Stock value'', to_char(sum(price * stock), ''FM999G999G990''), ''coins'' from sample_cards.product\nunion all\nselect ''Average rating'', to_char(avg(rating), ''FM0D0''), ''star'' from sample_cards.product\nunion all\nselect ''Out of stock'', count(*) filter (where stock = 0)::text, ''alert'' from sample_cards.product',
|
|
124
|
+
'{"style": "metric"}'),
|
|
125
|
+
(4, 20, 'Stock per category', 'cards', 12, 'standard',
|
|
126
|
+
E'select category as title, sum(stock)::text as badge,\n case category when ''Furniture'' then ''briefcase'' when ''Lighting'' then ''lightbulb'' when ''Plants'' then ''leaf'' else ''box'' end as icon\n from sample_cards.product group by category order by category',
|
|
127
|
+
'{"style": "metric"}'),
|
|
128
|
+
(4, 30, 'Products to reorder today', 'cards', 12, 'standard',
|
|
129
|
+
E'select name as title, stock || '' left'' as subtitle, ''alert'' as icon from sample_cards.product where stock < 0',
|
|
130
|
+
'{"empty": "Nothing to reorder: every product has stock. (This region''s query finds no rows, so it shows its empty text.)"}'),
|
|
131
|
+
|
|
132
|
+
-- 5 Lists as cards
|
|
133
|
+
(5, 5, 'About this page', 'static', 12, 'plain',
|
|
134
|
+
'<p class="lead">A <strong>list</strong> region shows a list from Shared Components → Lists with a template: <code>cards</code>, <code>badges</code>, <code>links</code> or <code>tabs</code>. The list here comes from a query (a dynamic list): a label, a page, an icon, a badge and a description per row.</p>',
|
|
135
|
+
'{}'),
|
|
136
|
+
(5, 10, 'Categories as cards', 'list', 12, 'standard', null, '{"list": "CATEGORIES", "template": "cards"}'),
|
|
137
|
+
(5, 20, 'Categories as badges', 'list', 6, 'standard', null, '{"list": "CATEGORIES", "template": "badges"}'),
|
|
138
|
+
(5, 30, 'Categories as links', 'list', 6, 'standard', null, '{"list": "CATEGORIES", "template": "links"}'),
|
|
139
|
+
(5, 40, 'Categories as tabs', 'list', 12, 'standard', null, '{"list": "CATEGORIES", "template": "tabs"}'),
|
|
140
|
+
|
|
141
|
+
-- 6 Template components
|
|
142
|
+
(6, 5, 'About this page', 'static', 12, 'plain',
|
|
143
|
+
'<p class="lead">A <strong>template component</strong> region draws each row of its query through a component; shown as <code>multiple</code>, the rows go into the component''s group. These are the built-in components every application has: <code>ut_metric_card</code>, <code>ut_media_list</code>, <code>ut_badge</code> and <code>ut_avatar</code>.</p>',
|
|
144
|
+
'{}'),
|
|
145
|
+
(6, 10, 'Metric cards', 'template_component', 12, 'standard',
|
|
146
|
+
E'select category as label, sum(stock)::text as value, ''in stock'' as unit,\n count(*) || '' products'' as change, case when min(stock) = 0 then ''down'' else ''up'' end as trend, ''up'' as good\n from sample_cards.product group by category order by category',
|
|
147
|
+
'{"component": "ut_metric_card", "display": "multiple"}'),
|
|
148
|
+
(6, 20, 'Media list', 'template_component', 6, 'standard',
|
|
149
|
+
E'select id, name as title, description, upper(left(name, 2)) as initials,\n to_char(price, ''FM999G990D00'') as badge, case when stock = 0 then ''danger'' when stock < 5 then ''warning'' else ''success'' end as state\n from sample_cards.product order by name limit 6',
|
|
150
|
+
'{"component": "ut_media_list", "display": "multiple", "link": {"page": 3, "items": {"P3_ID": "#id#"}}}'),
|
|
151
|
+
(6, 30, 'Badges', 'template_component', 6, 'standard',
|
|
152
|
+
E'select name as label, case when stock = 0 then ''danger'' when stock < 5 then ''warning'' else ''success'' end as state\n from sample_cards.product order by name',
|
|
153
|
+
'{"component": "ut_badge", "display": "multiple"}'),
|
|
154
|
+
(6, 40, 'Avatars', 'template_component', 12, 'standard',
|
|
155
|
+
E'select name, upper(left(name, 2)) as initials from sample_cards.product order by name',
|
|
156
|
+
'{"component": "ut_avatar", "display": "multiple", "attributes": {"SIZE": "medium"}}')
|
|
157
|
+
) as r (page, seq, title, type, cols, template, source, config);
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
-- =====================================================================
|
|
2
|
+
-- Sample app "Sample Charts" (App Gallery → Sample Apps)
|
|
3
|
+
--
|
|
4
|
+
-- Every chart type of the chart region, a page per kind of question
|
|
5
|
+
-- (comparing categories, change over time, parts of a whole, relations and
|
|
6
|
+
-- spread, plans) and a drill-down from a chart to a report. Each page starts
|
|
7
|
+
-- with a text that says how the query shapes the chart.
|
|
8
|
+
--
|
|
9
|
+
-- Installed by an administrator from the App Gallery, or with
|
|
10
|
+
-- psql "$DATABASE_URL" -1 -f examples/gallery/samples/sample-charts.sql
|
|
11
|
+
-- then open /a/sample-charts and sign in as demo / demo.
|
|
12
|
+
-- =====================================================================
|
|
13
|
+
|
|
14
|
+
-- helpers of this script (temporary: they go with the session)
|
|
15
|
+
create or replace function pg_temp.page_id(p_alias text, p_page int) returns int language sql stable as
|
|
16
|
+
$$ select p.id from meta.page p join meta.app a on a.id = p.app_id where a.alias = p_alias and p.page_no = p_page $$;
|
|
17
|
+
|
|
18
|
+
-- 1. Schema and data ---------------------------------------------------------
|
|
19
|
+
create schema sample_charts;
|
|
20
|
+
|
|
21
|
+
create table sample_charts.sale (
|
|
22
|
+
id int generated always as identity primary key,
|
|
23
|
+
sold_on date not null,
|
|
24
|
+
region text not null,
|
|
25
|
+
product text not null,
|
|
26
|
+
channel text not null,
|
|
27
|
+
units int not null,
|
|
28
|
+
amount numeric(12, 2) not null
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
-- twelve months of sales: four regions, three products, three channels
|
|
32
|
+
insert into sample_charts.sale (sold_on, region, product, channel, units, amount)
|
|
33
|
+
select (date_trunc('month', current_date) - make_interval(months => m))::date + ((r * 7 + p * 3) % 27)::int,
|
|
34
|
+
g.region, q.product, (array['Web', 'Store', 'Partner'])[1 + (m + r + p) % 3],
|
|
35
|
+
x.units, x.units * (array[640, 290, 95])[p]
|
|
36
|
+
from generate_series(0, 11) m,
|
|
37
|
+
unnest(array['North', 'South', 'East', 'West']) with ordinality as g (region, r),
|
|
38
|
+
unnest(array['Standing desk', 'Office chair', 'Monitor arm']) with ordinality as q (product, p),
|
|
39
|
+
lateral (select (5 + (m * 7 + r * 11 + p * 13) % 23)::int as units) x;
|
|
40
|
+
|
|
41
|
+
create table sample_charts.share_price (
|
|
42
|
+
day date primary key,
|
|
43
|
+
open numeric(8, 2) not null,
|
|
44
|
+
high numeric(8, 2) not null,
|
|
45
|
+
low numeric(8, 2) not null,
|
|
46
|
+
close numeric(8, 2) not null
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
insert into sample_charts.share_price (day, open, high, low, close)
|
|
50
|
+
select current_date - 30 + d, o, greatest(o, c) + 1.5, least(o, c) - 1.2, c
|
|
51
|
+
from generate_series(1, 30) d,
|
|
52
|
+
lateral (select round((100 + 8 * sin((d - 1) / 4.0) + (d - 1) * 0.3)::numeric, 2) as o,
|
|
53
|
+
round((100 + 8 * sin(d / 4.0) + d * 0.3)::numeric, 2) as c) v;
|
|
54
|
+
|
|
55
|
+
create table sample_charts.pipeline (
|
|
56
|
+
seq int primary key,
|
|
57
|
+
stage text not null,
|
|
58
|
+
deals int not null
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
insert into sample_charts.pipeline values (1, 'Leads', 240), (2, 'Qualified', 130), (3, 'Proposal', 64), (4, 'Negotiation', 31), (5, 'Won', 18);
|
|
62
|
+
|
|
63
|
+
create table sample_charts.budget (
|
|
64
|
+
node_id int primary key,
|
|
65
|
+
parent_id int references sample_charts.budget,
|
|
66
|
+
label text not null,
|
|
67
|
+
amount numeric(10, 2) not null default 0
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
insert into sample_charts.budget values
|
|
71
|
+
(1, null, 'Operations', 0), (2, null, 'Marketing', 0), (3, null, 'Engineering', 0),
|
|
72
|
+
(11, 1, 'Rent', 40), (12, 1, 'Utilities', 12),
|
|
73
|
+
(21, 2, 'Events', 18), (22, 2, 'Online ads', 25), (23, 2, 'Print', 6),
|
|
74
|
+
(31, 3, 'Salaries', 120), (32, 3, 'Cloud', 30), (33, 3, 'Tools', 14);
|
|
75
|
+
|
|
76
|
+
create table sample_charts.project_task (
|
|
77
|
+
id int primary key,
|
|
78
|
+
name text not null,
|
|
79
|
+
team text not null,
|
|
80
|
+
starts date not null,
|
|
81
|
+
ends date,
|
|
82
|
+
progress int not null default 0,
|
|
83
|
+
depends_on int[] not null default '{}'
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
insert into sample_charts.project_task values
|
|
87
|
+
(1, 'Kick-off', 'Product', current_date - 20, null, 100, '{}'),
|
|
88
|
+
(2, 'Requirements', 'Product', current_date - 19, current_date - 8, 100, '{1}'),
|
|
89
|
+
(3, 'Design', 'Product', current_date - 9, current_date + 3, 70, '{2}'),
|
|
90
|
+
(4, 'Build', 'Engineering', current_date + 1, current_date + 25, 10, '{3}'),
|
|
91
|
+
(5, 'Test', 'Quality', current_date + 18, current_date + 32, 0, '{4}'),
|
|
92
|
+
(6, 'Launch', 'Product', current_date + 33, null, 0, '{5}');
|
|
93
|
+
|
|
94
|
+
-- 2. The application's database role ----------------------------------------
|
|
95
|
+
do $$ begin
|
|
96
|
+
if not exists (select 1 from pg_roles where rolname = 'app_sample_charts') then create role app_sample_charts nologin; end if;
|
|
97
|
+
end $$;
|
|
98
|
+
grant app_sample_charts to pgkiln_runtime;
|
|
99
|
+
grant usage on schema sample_charts to app_sample_charts;
|
|
100
|
+
grant select on all tables in schema sample_charts to app_sample_charts;
|
|
101
|
+
|
|
102
|
+
-- 3. The application ----------------------------------------------------------
|
|
103
|
+
insert into meta.app (alias, name, home_page, authentication, db_role)
|
|
104
|
+
values ('sample-charts', 'Sample Charts', 1, 'app_users', 'app_sample_charts');
|
|
105
|
+
|
|
106
|
+
insert into meta.app_user (app_id, username, password_hash, roles)
|
|
107
|
+
select id, 'demo', meta.hash_password('demo'), '{}' from meta.app where alias = 'sample-charts';
|
|
108
|
+
|
|
109
|
+
insert into meta.page (app_id, page_no, name, title, parent_page)
|
|
110
|
+
select a.id, p.no, p.name, p.name, p.parent
|
|
111
|
+
from meta.app a, (values
|
|
112
|
+
(1, 'Sample Charts', null), (2, 'Comparing categories', 1), (3, 'Change over time', 1), (4, 'Parts of a whole', 1),
|
|
113
|
+
(5, 'Relations and spread', 1), (6, 'Plans and timelines', 1), (7, 'Drill-down', 1), (8, 'Sales by region', 7)
|
|
114
|
+
) as p (no, name, parent)
|
|
115
|
+
where a.alias = 'sample-charts';
|
|
116
|
+
|
|
117
|
+
insert into meta.nav_entry (app_id, seq, label, icon, target_page)
|
|
118
|
+
select a.id, n.seq, n.label, n.icon, n.page
|
|
119
|
+
from meta.app a, (values
|
|
120
|
+
(10, 'Home', 'home', 1), (20, 'Comparing categories', 'chart', 2), (30, 'Change over time', 'line-chart', 3),
|
|
121
|
+
(40, 'Parts of a whole', 'pie-chart', 4), (50, 'Relations and spread', 'activity', 5), (60, 'Plans and timelines', 'calendar', 6),
|
|
122
|
+
(70, 'Drill-down', 'filter', 7)
|
|
123
|
+
) as n (seq, label, icon, page)
|
|
124
|
+
where a.alias = 'sample-charts';
|
|
125
|
+
|
|
126
|
+
-- the home page's list of topics
|
|
127
|
+
insert into meta.list (app_id, name, type, description)
|
|
128
|
+
select id, 'CHART_TOPICS', 'static', 'The pages of Sample Charts (cards on the home page)' from meta.app where alias = 'sample-charts';
|
|
129
|
+
|
|
130
|
+
insert into meta.list_entry (app_id, list_name, seq, label, icon, target_page, description)
|
|
131
|
+
select a.id, 'CHART_TOPICS', e.seq, e.label, e.icon, e.page, e.description
|
|
132
|
+
from meta.app a, (values
|
|
133
|
+
(10, 'Comparing categories', 'chart', 2, 'Bar, column, stacked and combination charts.'),
|
|
134
|
+
(20, 'Change over time', 'line-chart', 3, 'Line, area, range and stock charts.'),
|
|
135
|
+
(30, 'Parts of a whole', 'pie-chart', 4, 'Pie, donut, funnel, pyramid, treemap and sunburst charts.'),
|
|
136
|
+
(40, 'Relations and spread', 'activity', 5, 'Scatter, bubble, box plot, radar, polar and gauge charts.'),
|
|
137
|
+
(50, 'Plans and timelines', 'calendar', 6, 'Gantt and timeline charts.'),
|
|
138
|
+
(60, 'Drill-down', 'filter', 7, 'A chart whose columns open a report of their rows.')
|
|
139
|
+
) as e (seq, label, icon, page, description)
|
|
140
|
+
where a.alias = 'sample-charts';
|
|
141
|
+
|
|
142
|
+
insert into meta.region (page_id, seq, title, type, columns, template, source, config)
|
|
143
|
+
select pg_temp.page_id('sample-charts', r.page), r.seq, r.title, r.type, r.cols, r.template, r.source, r.config::jsonb
|
|
144
|
+
from (values
|
|
145
|
+
-- 1 Home
|
|
146
|
+
(1, 10, 'Sample Charts', 'static', 12, 'hero',
|
|
147
|
+
'<p>A chart region draws a SELECT: the first column is the label, every further number column a series, named by its alias. The attribute <code>kind</code> chooses one of 22 chart types. Every chart has tooltips, a legend for several series and a <em>Data table</em> toggle, the accessible alternative to the picture.</p>',
|
|
148
|
+
'{"icon": "chart"}'),
|
|
149
|
+
(1, 20, 'Topics', 'list', 12, 'plain', null, '{"list": "CHART_TOPICS", "template": "cards"}'),
|
|
150
|
+
(1, 30, 'The data', 'static', 12, 'standard',
|
|
151
|
+
'<p>The schema <code>sample_charts</code> holds a year of sales (<code>sale</code>: region, product, channel, units, amount), a month of share prices, a sales pipeline, a budget in two levels and a small project plan. The queries of the charts are in the Page Designer: open a region and look at its Source and its attributes.</p>',
|
|
152
|
+
'{}'),
|
|
153
|
+
|
|
154
|
+
-- 2 Comparing categories
|
|
155
|
+
(2, 5, 'About these charts', 'static', 12, 'plain',
|
|
156
|
+
'<p class="lead">Compare categories with bars and columns. <strong>bar</strong> draws horizontal bars, good for long labels; <strong>column</strong> vertical columns; <strong>stacked</strong> stacks the series of a label on top of each other; <strong>combo</strong> draws the first series as columns and the others as lines over them.</p>',
|
|
157
|
+
'{}'),
|
|
158
|
+
(2, 10, 'Units sold per product and channel (bar)', 'chart', 6, 'standard',
|
|
159
|
+
E'select product || '' via '' || lower(channel) as product, sum(units) as "Units"\n from sample_charts.sale group by product, channel order by 2 desc',
|
|
160
|
+
'{"kind": "bar"}'),
|
|
161
|
+
(2, 20, 'Sales per region (column)', 'chart', 6, 'standard',
|
|
162
|
+
E'select region, sum(amount) as "Sales"\n from sample_charts.sale group by region order by region',
|
|
163
|
+
'{"kind": "column", "format_mask": "999G999G990"}'),
|
|
164
|
+
(2, 30, 'Sales per region and product (stacked)', 'chart', 6, 'standard',
|
|
165
|
+
E'select region,\n sum(amount) filter (where product = ''Standing desk'') as "Standing desk",\n sum(amount) filter (where product = ''Office chair'') as "Office chair",\n sum(amount) filter (where product = ''Monitor arm'') as "Monitor arm"\n from sample_charts.sale group by region order by region',
|
|
166
|
+
'{"kind": "stacked", "format_mask": "999G999G990"}'),
|
|
167
|
+
(2, 40, 'Sales against the target (combo)', 'chart', 6, 'standard',
|
|
168
|
+
E'select region, sum(amount) as "Sales", 200000 as "Target"\n from sample_charts.sale group by region order by region',
|
|
169
|
+
'{"kind": "combo", "format_mask": "999G999G990"}'),
|
|
170
|
+
|
|
171
|
+
-- 3 Change over time
|
|
172
|
+
(3, 5, 'About these charts', 'static', 12, 'plain',
|
|
173
|
+
'<p class="lead">Show change over time with the label column in order (months, days). <strong>line</strong> and <strong>area</strong> draw one line per series; <strong>range</strong> a floating column from a low to a high column; <strong>stock</strong> a candle per row from the open, high, low and close columns.</p>',
|
|
174
|
+
'{}'),
|
|
175
|
+
(3, 10, 'Monthly sales per region (line)', 'chart', 12, 'standard',
|
|
176
|
+
E'select to_char(date_trunc(''month'', sold_on), ''YYYY-MM'') as month,\n sum(amount) filter (where region = ''North'') as "North",\n sum(amount) filter (where region = ''South'') as "South",\n sum(amount) filter (where region = ''East'') as "East",\n sum(amount) filter (where region = ''West'') as "West"\n from sample_charts.sale group by 1 order by 1',
|
|
177
|
+
'{"kind": "line", "format_mask": "999G999G990"}'),
|
|
178
|
+
(3, 20, 'Sales so far this year (area)', 'chart', 6, 'standard',
|
|
179
|
+
E'select month, sum(total) over (order by month) as "Cumulative sales"\n from (select to_char(date_trunc(''month'', sold_on), ''YYYY-MM'') as month, sum(amount) as total\n from sample_charts.sale group by 1) m\n order by month',
|
|
180
|
+
'{"kind": "area", "format_mask": "999G999G990"}'),
|
|
181
|
+
(3, 30, 'Smallest and largest sale per region (range)', 'chart', 6, 'standard',
|
|
182
|
+
E'select region, min(amount) as "Smallest", max(amount) as "Largest"\n from sample_charts.sale group by region order by region',
|
|
183
|
+
'{"kind": "range", "format_mask": "999G999G990"}'),
|
|
184
|
+
(3, 40, 'Share price over the last month (stock)', 'chart', 12, 'standard',
|
|
185
|
+
E'select to_char(day, ''DD Mon'') as day, open as "Open", high as "High", low as "Low", close as "Close"\n from sample_charts.share_price order by sample_charts.share_price.day',
|
|
186
|
+
'{"kind": "stock"}'),
|
|
187
|
+
|
|
188
|
+
-- 4 Parts of a whole
|
|
189
|
+
(4, 5, 'About these charts', 'static', 12, 'plain',
|
|
190
|
+
'<p class="lead">Show how a total divides. <strong>pie</strong> and <strong>donut</strong> use the first series (up to six slices, the rest folds into Other); <strong>funnel</strong> shows the stages of a process in the order of the query; <strong>pyramid</strong> with two series compares two groups back to back; <strong>treemap</strong> and <strong>sunburst</strong> show a hierarchy from the columns <code>node_id</code> and <code>parent_id</code>.</p>',
|
|
191
|
+
'{}'),
|
|
192
|
+
(4, 10, 'Sales per channel (pie)', 'chart', 6, 'standard',
|
|
193
|
+
E'select channel, sum(amount) as "Sales"\n from sample_charts.sale group by channel order by 2 desc',
|
|
194
|
+
'{"kind": "pie", "format_mask": "999G999G990"}'),
|
|
195
|
+
(4, 20, 'Units per product (donut)', 'chart', 6, 'standard',
|
|
196
|
+
E'select product, sum(units) as "Units"\n from sample_charts.sale group by product order by 2 desc',
|
|
197
|
+
'{"kind": "donut"}'),
|
|
198
|
+
(4, 30, 'Sales pipeline (funnel)', 'chart', 6, 'standard',
|
|
199
|
+
E'select stage, deals as "Deals" from sample_charts.pipeline order by seq',
|
|
200
|
+
'{"kind": "funnel"}'),
|
|
201
|
+
(4, 40, 'Units per product, North against South (pyramid)', 'chart', 6, 'standard',
|
|
202
|
+
E'select product,\n sum(units) filter (where region = ''North'') as "North",\n sum(units) filter (where region = ''South'') as "South"\n from sample_charts.sale group by product order by product',
|
|
203
|
+
'{"kind": "pyramid"}'),
|
|
204
|
+
(4, 50, 'Budget (treemap)', 'chart', 6, 'standard',
|
|
205
|
+
E'select label, amount as "Budget", node_id, parent_id from sample_charts.budget order by node_id',
|
|
206
|
+
'{"kind": "treemap"}'),
|
|
207
|
+
(4, 60, 'Budget (sunburst)', 'chart', 6, 'standard',
|
|
208
|
+
E'select label, amount as "Budget", node_id, parent_id from sample_charts.budget order by node_id',
|
|
209
|
+
'{"kind": "sunburst"}'),
|
|
210
|
+
|
|
211
|
+
-- 5 Relations and spread
|
|
212
|
+
(5, 5, 'About these charts', 'static', 12, 'plain',
|
|
213
|
+
'<p class="lead">Look at numbers against each other. <strong>scatter</strong> takes a number as its first column (the x axis); <strong>bubble</strong> a label, then x, y and size; <strong>boxplot</strong> one row per observation and computes the quartiles per label; <strong>radar</strong> an axis per row; <strong>polar</strong> a sector per row; <strong>gauge</strong> a dial per row with thresholds in its attributes.</p>',
|
|
214
|
+
'{}'),
|
|
215
|
+
(5, 10, 'Units against amount per sale (scatter)', 'chart', 6, 'standard',
|
|
216
|
+
E'select units as "Units", amount as "Amount" from sample_charts.sale order by units',
|
|
217
|
+
'{"kind": "scatter"}'),
|
|
218
|
+
(5, 20, 'Products per region: units, average sale and number of sales (bubble)', 'chart', 6, 'standard',
|
|
219
|
+
E'select region || '' '' || lower(product) as product, sum(units) as "Units", round(avg(amount)) as "Average sale", count(*) as "Sales"\n from sample_charts.sale group by region, product order by 1',
|
|
220
|
+
'{"kind": "bubble"}'),
|
|
221
|
+
(5, 30, 'Spread of the sales per region (box plot)', 'chart', 6, 'standard',
|
|
222
|
+
E'select region, amount as "Sale" from sample_charts.sale order by region',
|
|
223
|
+
'{"kind": "boxplot"}'),
|
|
224
|
+
(5, 40, 'Units per region and product (radar)', 'chart', 6, 'standard',
|
|
225
|
+
E'select region,\n sum(units) filter (where product = ''Standing desk'') as "Standing desk",\n sum(units) filter (where product = ''Office chair'') as "Office chair",\n sum(units) filter (where product = ''Monitor arm'') as "Monitor arm"\n from sample_charts.sale group by region order by region',
|
|
226
|
+
'{"kind": "radar"}'),
|
|
227
|
+
(5, 50, 'Sales per month (polar)', 'chart', 6, 'standard',
|
|
228
|
+
E'select to_char(date_trunc(''month'', sold_on), ''Mon'') as month, sum(amount) as "Sales"\n from sample_charts.sale group by date_trunc(''month'', sold_on) order by date_trunc(''month'', sold_on)',
|
|
229
|
+
'{"kind": "polar", "format_mask": "999G999G990"}'),
|
|
230
|
+
(5, 60, 'Target reached per region, % (gauge)', 'chart', 6, 'standard',
|
|
231
|
+
E'select region, round(100.0 * sum(amount) / 200000) as "Target reached"\n from sample_charts.sale group by region order by region',
|
|
232
|
+
'{"kind": "gauge", "gauge": {"min": 0, "max": 120, "warning": 90, "critical": 75}}'),
|
|
233
|
+
|
|
234
|
+
-- 6 Plans and timelines
|
|
235
|
+
(6, 5, 'About these charts', 'static', 12, 'plain',
|
|
236
|
+
'<p class="lead">Show plans. <strong>gantt</strong> takes a label, a start and an end (no end: a milestone), and optional columns <code>progress</code>, <code>task_id</code> and <code>depends_on</code>; <strong>timeline</strong> a label, a start and an end, with a lane per value of the column <code>series</code>.</p>',
|
|
237
|
+
'{}'),
|
|
238
|
+
(6, 10, 'Project plan (Gantt)', 'chart', 12, 'standard',
|
|
239
|
+
E'select name as "Task", starts as "Starts", ends as "Ends", progress, id as task_id,\n array_to_string(depends_on, '','') as depends_on\n from sample_charts.project_task order by starts, id',
|
|
240
|
+
'{"kind": "gantt"}'),
|
|
241
|
+
(6, 20, 'Who works when (timeline)', 'chart', 12, 'standard',
|
|
242
|
+
E'select name as "Task", starts as "Starts", ends as "Ends", team as series\n from sample_charts.project_task order by starts, id',
|
|
243
|
+
'{"kind": "timeline"}'),
|
|
244
|
+
|
|
245
|
+
-- 7 Drill-down
|
|
246
|
+
(7, 5, 'About this page', 'static', 12, 'plain',
|
|
247
|
+
'<p class="lead">With the attribute <code>link</code> every column of the chart is a link: <code>#region#</code> is replaced by the value of the clicked row. The link carries a checksum, like every link pgkiln makes. Columns that only the link uses are not drawn. From the keyboard, the data table has the same links.</p>',
|
|
248
|
+
'{}'),
|
|
249
|
+
(7, 10, 'Sales per region: click a column', 'chart', 12, 'standard',
|
|
250
|
+
E'select region, sum(amount) as "Sales"\n from sample_charts.sale group by region order by region',
|
|
251
|
+
'{"kind": "column", "format_mask": "999G999G990", "link": {"page": 8, "items": {"P8_REGION": "#region#"}}}'),
|
|
252
|
+
|
|
253
|
+
-- 8 Sales by region (the drill-down's target)
|
|
254
|
+
(8, 10, 'Sales', 'report', 12, 'standard',
|
|
255
|
+
E'select sold_on, region, product, channel, units, amount\n from sample_charts.sale\n where :P8_REGION is null or region = :P8_REGION\n order by sold_on desc, id',
|
|
256
|
+
'{"headings": {"sold_on": "Sold on"}, "formats": {"amount": "999G999G990D00"}}'),
|
|
257
|
+
(8, 20, 'Per product', 'chart', 12, 'standard',
|
|
258
|
+
E'select product, sum(amount) as "Sales"\n from sample_charts.sale where :P8_REGION is null or region = :P8_REGION\n group by product order by 2 desc',
|
|
259
|
+
'{"kind": "bar", "format_mask": "999G999G990"}')
|
|
260
|
+
) as r (page, seq, title, type, cols, template, source, config);
|
|
261
|
+
|
|
262
|
+
insert into meta.item (page_id, region_id, seq, name, label, type, lov, config)
|
|
263
|
+
select pg_temp.page_id('sample-charts', 8), null, 10, 'P8_REGION', 'Region', 'select',
|
|
264
|
+
'select distinct region as d, region as r from sample_charts.sale order by 1',
|
|
265
|
+
'{"submit_on_change": true, "null_label": "All regions"}';
|