dataops-testgen 2.2.0__py3-none-any.whl
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.
- dataops_testgen-2.2.0.dist-info/LICENSE +203 -0
- dataops_testgen-2.2.0.dist-info/METADATA +287 -0
- dataops_testgen-2.2.0.dist-info/NOTICE +5 -0
- dataops_testgen-2.2.0.dist-info/RECORD +270 -0
- dataops_testgen-2.2.0.dist-info/WHEEL +5 -0
- dataops_testgen-2.2.0.dist-info/entry_points.txt +2 -0
- dataops_testgen-2.2.0.dist-info/top_level.txt +1 -0
- testgen/__init__.py +0 -0
- testgen/__main__.py +770 -0
- testgen/commands/__init__.py +0 -0
- testgen/commands/queries/__init__.py +0 -0
- testgen/commands/queries/execute_cat_tests_query.py +95 -0
- testgen/commands/queries/execute_tests_query.py +160 -0
- testgen/commands/queries/generate_tests_query.py +94 -0
- testgen/commands/queries/profiling_query.py +366 -0
- testgen/commands/queries/test_parameter_validation_query.py +88 -0
- testgen/commands/run_execute_cat_tests.py +162 -0
- testgen/commands/run_execute_tests.py +168 -0
- testgen/commands/run_generate_tests.py +107 -0
- testgen/commands/run_get_entities.py +122 -0
- testgen/commands/run_launch_db_config.py +84 -0
- testgen/commands/run_observability_exporter.py +330 -0
- testgen/commands/run_profiling_bridge.py +495 -0
- testgen/commands/run_quick_start.py +168 -0
- testgen/commands/run_setup_profiling_tools.py +96 -0
- testgen/commands/run_test_definition.py +146 -0
- testgen/commands/run_test_parameter_validation.py +135 -0
- testgen/commands/run_upgrade_db_config.py +156 -0
- testgen/common/__init__.py +8 -0
- testgen/common/clean_sql.py +53 -0
- testgen/common/credentials.py +25 -0
- testgen/common/database/__init__.py +0 -0
- testgen/common/database/database_service.py +629 -0
- testgen/common/database/flavor/__init__.py +0 -0
- testgen/common/database/flavor/flavor_service.py +75 -0
- testgen/common/database/flavor/mssql_flavor_service.py +34 -0
- testgen/common/database/flavor/postgresql_flavor_service.py +5 -0
- testgen/common/database/flavor/redshift_flavor_service.py +22 -0
- testgen/common/database/flavor/snowflake_flavor_service.py +69 -0
- testgen/common/database/flavor/trino_flavor_service.py +21 -0
- testgen/common/date_service.py +68 -0
- testgen/common/display_service.py +85 -0
- testgen/common/docker_service.py +76 -0
- testgen/common/encrypt.py +55 -0
- testgen/common/get_pipeline_parms.py +57 -0
- testgen/common/logs.py +79 -0
- testgen/common/process_service.py +62 -0
- testgen/common/read_file.py +69 -0
- testgen/settings.py +440 -0
- testgen/template/dbsetup/010_create_base_schema.sql +2 -0
- testgen/template/dbsetup/020_create_standard_functions_sprocs.sql +179 -0
- testgen/template/dbsetup/030_initialize_new_schema_structure.sql +735 -0
- testgen/template/dbsetup/040_populate_new_schema_project.sql +59 -0
- testgen/template/dbsetup/050_populate_new_schema_metadata.sql +1517 -0
- testgen/template/dbsetup/060_create_standard_views.sql +248 -0
- testgen/template/dbsetup/070_create_default_users.sql +17 -0
- testgen/template/dbsetup/075_grant_role_rights.sql +43 -0
- testgen/template/dbsetup/080_set_current_revision.sql +5 -0
- testgen/template/dbupgrade/0100_incremental_upgrade.sql +5 -0
- testgen/template/dbupgrade/0101_incremental_upgrade.sql +15 -0
- testgen/template/dbupgrade/0102_incremental_upgrade.sql +4 -0
- testgen/template/dbupgrade/0103_incremental_upgrade.sql +22 -0
- testgen/template/dbupgrade/0104_incremental_upgrade.sql +44 -0
- testgen/template/dbupgrade/0105_incremental_upgrade.sql +1 -0
- testgen/template/dbupgrade/0106_incremental_upgrade.sql +5 -0
- testgen/template/dbupgrade/0107_incremental_upgrade.sql +3 -0
- testgen/template/dbupgrade_helpers/get_tg_revision.sql +2 -0
- testgen/template/exec_cat_tests/ex_cat_build_agg_table_tests.sql +116 -0
- testgen/template/exec_cat_tests/ex_cat_get_distinct_tables.sql +11 -0
- testgen/template/exec_cat_tests/ex_cat_results_parse.sql +69 -0
- testgen/template/exec_cat_tests/ex_cat_retrieve_agg_test_parms.sql +6 -0
- testgen/template/exec_cat_tests/ex_cat_test_query.sql +8 -0
- testgen/template/execution/ex_finalize_test_run_results.sql +37 -0
- testgen/template/execution/ex_get_tests_non_cat.sql +47 -0
- testgen/template/execution/ex_update_test_record_in_testrun_table.sql +27 -0
- testgen/template/execution/ex_write_test_record_to_testrun_table.sql +6 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_no_drops_generic.sql +48 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_num_incr_generic.sql +34 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_percent_above_generic.sql +49 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_percent_within_generic.sql +49 -0
- testgen/template/flavors/generic/exec_query_tests/ex_aggregate_match_same_generic.sql +49 -0
- testgen/template/flavors/generic/exec_query_tests/ex_custom_query_generic.sql +39 -0
- testgen/template/flavors/generic/exec_query_tests/ex_data_match_2way_generic.sql +58 -0
- testgen/template/flavors/generic/exec_query_tests/ex_data_match_generic.sql +44 -0
- testgen/template/flavors/generic/exec_query_tests/ex_prior_match_generic.sql +37 -0
- testgen/template/flavors/generic/exec_query_tests/ex_relative_entropy_generic.sql +53 -0
- testgen/template/flavors/generic/exec_query_tests/ex_window_match_no_drops_generic.sql +46 -0
- testgen/template/flavors/generic/exec_query_tests/ex_window_match_same_generic.sql +59 -0
- testgen/template/flavors/generic/profiling/contingency_counts.sql +3 -0
- testgen/template/flavors/generic/validate_tests/ex_get_project_column_list_generic.sql +3 -0
- testgen/template/flavors/mssql/exec_query_tests/ex_relative_entropy_mssql.sql +53 -0
- testgen/template/flavors/mssql/profiling/project_ddf_query_mssql.sql +35 -0
- testgen/template/flavors/mssql/profiling/project_profiling_query_mssql.yaml +246 -0
- testgen/template/flavors/mssql/profiling/project_secondary_profiling_query_mssql.sql +36 -0
- testgen/template/flavors/mssql/setup_profiling_tools/00_drop_existing_functions_mssql.sql +8 -0
- testgen/template/flavors/mssql/setup_profiling_tools/01_create_functions_mssql.sql +12 -0
- testgen/template/flavors/mssql/setup_profiling_tools/02_create_functions_mssql.sql +54 -0
- testgen/template/flavors/mssql/setup_profiling_tools/create_qc_schema_mssql.sql +4 -0
- testgen/template/flavors/mssql/setup_profiling_tools/grant_execute_privileges_mssql.sql +1 -0
- testgen/template/flavors/postgresql/exec_query_tests/ex_window_match_no_drops_postgresql.sql +46 -0
- testgen/template/flavors/postgresql/exec_query_tests/ex_window_match_same_postgresql.sql +59 -0
- testgen/template/flavors/postgresql/profiling/project_ddf_query_postgresql.sql +42 -0
- testgen/template/flavors/postgresql/profiling/project_profiling_query_postgresql.yaml +225 -0
- testgen/template/flavors/postgresql/profiling/project_secondary_profiling_query_postgresql.sql +28 -0
- testgen/template/flavors/postgresql/setup_profiling_tools/create_functions_postgresql.sql +157 -0
- testgen/template/flavors/postgresql/setup_profiling_tools/create_qc_schema_postgresql.sql +1 -0
- testgen/template/flavors/postgresql/setup_profiling_tools/grant_execute_privileges_postgresql.sql +2 -0
- testgen/template/flavors/redshift/profiling/project_ddf_query_redshift.sql +38 -0
- testgen/template/flavors/redshift/profiling/project_profiling_query_redshift.yaml +221 -0
- testgen/template/flavors/redshift/profiling/project_secondary_profiling_query_redshift.sql +29 -0
- testgen/template/flavors/redshift/setup_profiling_tools/create_functions_redshift.sql +115 -0
- testgen/template/flavors/redshift/setup_profiling_tools/create_qc_schema_redshift.sql +1 -0
- testgen/template/flavors/redshift/setup_profiling_tools/grant_execute_privileges_redshift.sql +2 -0
- testgen/template/flavors/snowflake/profiling/project_ddf_query_snowflake.sql +38 -0
- testgen/template/flavors/snowflake/profiling/project_profiling_query_snowflake.yaml +220 -0
- testgen/template/flavors/snowflake/profiling/project_secondary_profiling_query_snowflake.sql +29 -0
- testgen/template/flavors/snowflake/setup_profiling_tools/create_functions_snowflake.sql +69 -0
- testgen/template/flavors/snowflake/setup_profiling_tools/create_qc_schema_snowflake.sql +1 -0
- testgen/template/flavors/snowflake/setup_profiling_tools/grant_execute_privileges_snowflake.sql +6 -0
- testgen/template/flavors/trino/profiling/project_profiling_query_trino.yaml +219 -0
- testgen/template/flavors/trino/setup_profiling_tools/create_functions_trino.sql +92 -0
- testgen/template/flavors/trino/setup_profiling_tools/create_qc_schema_trino.sql +1 -0
- testgen/template/gen_funny_cat_tests/gen_test_constant.sql +104 -0
- testgen/template/gen_funny_cat_tests/gen_test_distinct_value_ct.sql +98 -0
- testgen/template/gen_funny_cat_tests/gen_test_row_ct.sql +57 -0
- testgen/template/gen_funny_cat_tests/gen_test_row_ct_pct.sql +59 -0
- testgen/template/generation/gen_delete_old_tests.sql +5 -0
- testgen/template/generation/gen_insert_test_suite.sql +5 -0
- testgen/template/generation/gen_retrieve_or_insert_test_suite.sql +58 -0
- testgen/template/generation/gen_standard_test_type_list.sql +13 -0
- testgen/template/generation/gen_standard_tests.sql +48 -0
- testgen/template/get_entities/get_connection.sql +21 -0
- testgen/template/get_entities/get_connections_list.sql +9 -0
- testgen/template/get_entities/get_latest.sql +4 -0
- testgen/template/get_entities/get_profile.sql +12 -0
- testgen/template/get_entities/get_profile_info.sql +17 -0
- testgen/template/get_entities/get_profile_list.sql +17 -0
- testgen/template/get_entities/get_profile_screen.sql +275 -0
- testgen/template/get_entities/get_project_list.sql +6 -0
- testgen/template/get_entities/get_table_group_list.sql +10 -0
- testgen/template/get_entities/get_test_generation_list.sql +18 -0
- testgen/template/get_entities/get_test_info.sql +41 -0
- testgen/template/get_entities/get_test_results_for_run_cli.sql +16 -0
- testgen/template/get_entities/get_test_run_list.sql +24 -0
- testgen/template/get_entities/get_test_suite.sql +13 -0
- testgen/template/get_entities/get_test_suite_list.sql +18 -0
- testgen/template/get_entities/list_test_types.sql +4 -0
- testgen/template/observability/get_event_data.sql +23 -0
- testgen/template/observability/get_test_results.sql +41 -0
- testgen/template/observability/update_test_results_exported_to_observability.sql +12 -0
- testgen/template/parms/parms_profiling.sql +34 -0
- testgen/template/parms/parms_test_execution.sql +13 -0
- testgen/template/parms/parms_test_gen.sql +23 -0
- testgen/template/profiling/contingency_columns.sql +7 -0
- testgen/template/profiling/datatype_suggestions.sql +56 -0
- testgen/template/profiling/functional_datatype.sql +523 -0
- testgen/template/profiling/functional_tabletype_stage.sql +48 -0
- testgen/template/profiling/functional_tabletype_update.sql +8 -0
- testgen/template/profiling/pii_flag.sql +133 -0
- testgen/template/profiling/profile_anomalies_screen_column.sql +22 -0
- testgen/template/profiling/profile_anomalies_screen_multi_column.sql +58 -0
- testgen/template/profiling/profile_anomalies_screen_table.sql +22 -0
- testgen/template/profiling/profile_anomalies_screen_table_dates.sql +30 -0
- testgen/template/profiling/profile_anomalies_screen_variants.sql +40 -0
- testgen/template/profiling/profile_anomaly_types_get.sql +3 -0
- testgen/template/profiling/project_get_table_sample_count.sql +22 -0
- testgen/template/profiling/project_profile_run_record_insert.sql +8 -0
- testgen/template/profiling/project_profile_run_record_update.sql +5 -0
- testgen/template/profiling/project_profile_run_record_update_status.sql +5 -0
- testgen/template/profiling/project_update_profile_results_to_estimates.sql +32 -0
- testgen/template/profiling/refresh_anomalies.sql +33 -0
- testgen/template/profiling/refresh_data_chars_from_profiling.sql +156 -0
- testgen/template/profiling/secondary_profiling_columns.sql +12 -0
- testgen/template/profiling/secondary_profiling_delete.sql +4 -0
- testgen/template/profiling/secondary_profiling_update.sql +18 -0
- testgen/template/quick_start/populate_target_data.sql +1077 -0
- testgen/template/quick_start/recreate_target_data_schema.sql +167 -0
- testgen/template/quick_start/update_target_data.sql +100 -0
- testgen/template/updates/create_tmp_test_definition.sql +19 -0
- testgen/template/updates/get_test_def_parms.sql +38 -0
- testgen/template/updates/populate_stg_test_definitions.sql +184 -0
- testgen/template/validate_tests/ex_disable_tests_test_definitions.sql +5 -0
- testgen/template/validate_tests/ex_flag_tests_test_definitions.sql +64 -0
- testgen/template/validate_tests/ex_get_project_column_list_generic.sql +3 -0
- testgen/template/validate_tests/ex_get_test_column_list_tg.sql +65 -0
- testgen/template/validate_tests/ex_write_test_val_errors.sql +22 -0
- testgen/ui/__init__.py +0 -0
- testgen/ui/app.py +98 -0
- testgen/ui/assets/dk_logo.svg +46 -0
- testgen/ui/assets/question_mark.png +0 -0
- testgen/ui/assets/scripts.js +68 -0
- testgen/ui/assets/style.css +140 -0
- testgen/ui/bootstrap.py +109 -0
- testgen/ui/components/__init__.py +0 -0
- testgen/ui/components/frontend/css/KFOlCnqEu92Fr1MmEU9fBBc4.woff2 +0 -0
- testgen/ui/components/frontend/css/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2 +0 -0
- testgen/ui/components/frontend/css/KFOmCnqEu92Fr1Mu4mxK.woff2 +0 -0
- testgen/ui/components/frontend/css/KFOmCnqEu92Fr1Mu7GxKOzY.woff2 +0 -0
- testgen/ui/components/frontend/css/material-symbols-rounded.css +24 -0
- testgen/ui/components/frontend/css/material-symbols-rounded.woff2 +0 -0
- testgen/ui/components/frontend/css/roboto-font-faces.css +35 -0
- testgen/ui/components/frontend/css/shared.css +36 -0
- testgen/ui/components/frontend/img/dk_logo.svg +46 -0
- testgen/ui/components/frontend/index.html +17 -0
- testgen/ui/components/frontend/js/components/breadcrumbs.js +86 -0
- testgen/ui/components/frontend/js/components/button.js +66 -0
- testgen/ui/components/frontend/js/components/location.js +62 -0
- testgen/ui/components/frontend/js/components/select.js +75 -0
- testgen/ui/components/frontend/js/components/sidebar.js +358 -0
- testgen/ui/components/frontend/js/main.js +99 -0
- testgen/ui/components/frontend/js/streamlit.js +19 -0
- testgen/ui/components/frontend/js/van.min.js +1 -0
- testgen/ui/components/utils/__init__.py +0 -0
- testgen/ui/components/utils/callbacks.py +51 -0
- testgen/ui/components/utils/component.py +13 -0
- testgen/ui/components/widgets/__init__.py +6 -0
- testgen/ui/components/widgets/breadcrumbs.py +32 -0
- testgen/ui/components/widgets/location.py +65 -0
- testgen/ui/components/widgets/modal.py +97 -0
- testgen/ui/components/widgets/sidebar.py +69 -0
- testgen/ui/navigation/__init__.py +0 -0
- testgen/ui/navigation/menu.py +42 -0
- testgen/ui/navigation/page.py +20 -0
- testgen/ui/navigation/router.py +63 -0
- testgen/ui/queries/__init__.py +0 -0
- testgen/ui/queries/authentication_queries.py +47 -0
- testgen/ui/queries/connection_queries.py +121 -0
- testgen/ui/queries/profiling_queries.py +148 -0
- testgen/ui/queries/project_queries.py +9 -0
- testgen/ui/queries/table_group_queries.py +186 -0
- testgen/ui/queries/test_definition_queries.py +270 -0
- testgen/ui/queries/test_run_queries.py +32 -0
- testgen/ui/queries/test_suite_queries.py +145 -0
- testgen/ui/scripts/__init__.py +0 -0
- testgen/ui/scripts/patch_streamlit.py +111 -0
- testgen/ui/services/__init__.py +0 -0
- testgen/ui/services/authentication_service.py +119 -0
- testgen/ui/services/connection_service.py +220 -0
- testgen/ui/services/database_service.py +282 -0
- testgen/ui/services/form_service.py +1008 -0
- testgen/ui/services/javascript_service.py +44 -0
- testgen/ui/services/query_service.py +316 -0
- testgen/ui/services/string_service.py +12 -0
- testgen/ui/services/table_group_service.py +130 -0
- testgen/ui/services/test_definition_service.py +117 -0
- testgen/ui/services/test_run_service.py +13 -0
- testgen/ui/services/test_suite_service.py +76 -0
- testgen/ui/services/toolbar_service.py +77 -0
- testgen/ui/session.py +46 -0
- testgen/ui/views/__init__.py +0 -0
- testgen/ui/views/app_log_modal.py +92 -0
- testgen/ui/views/connections.py +72 -0
- testgen/ui/views/connections_base.py +367 -0
- testgen/ui/views/login.py +40 -0
- testgen/ui/views/not_found.py +16 -0
- testgen/ui/views/overview.py +34 -0
- testgen/ui/views/profiling_anomalies.py +501 -0
- testgen/ui/views/profiling_details.py +335 -0
- testgen/ui/views/profiling_modal.py +40 -0
- testgen/ui/views/profiling_results.py +206 -0
- testgen/ui/views/profiling_summary.py +177 -0
- testgen/ui/views/project_settings.py +74 -0
- testgen/ui/views/table_groups.py +530 -0
- testgen/ui/views/test_definitions.py +1020 -0
- testgen/ui/views/test_results.py +908 -0
- testgen/ui/views/test_runs.py +195 -0
- testgen/ui/views/test_suites.py +545 -0
- testgen/utils/__init__.py +0 -0
- testgen/utils/plugins.py +17 -0
- testgen/utils/singleton.py +14 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef Properties
|
|
3
|
+
* @type {object}
|
|
4
|
+
* @property {string} id - id of the specific component to be rendered
|
|
5
|
+
* @property {string} key - user key of the specific component to be rendered
|
|
6
|
+
* @property {object} props - object with the props to pass to the rendered component
|
|
7
|
+
*/
|
|
8
|
+
import van from './van.min.js';
|
|
9
|
+
import { Streamlit } from './streamlit.js';
|
|
10
|
+
import { Button } from './components/button.js'
|
|
11
|
+
import { Select } from './components/select.js'
|
|
12
|
+
import { Location } from './components/location.js'
|
|
13
|
+
import { Breadcrumbs } from './components/breadcrumbs.js'
|
|
14
|
+
|
|
15
|
+
let currentWindowVan = van;
|
|
16
|
+
let topWindowVan = window.top.van;
|
|
17
|
+
|
|
18
|
+
const TestGenComponent = (/** @type {string} */ id, /** @type {object} */ props) => {
|
|
19
|
+
const componentById = {
|
|
20
|
+
select: Button,
|
|
21
|
+
button: Select,
|
|
22
|
+
location: Location,
|
|
23
|
+
breadcrumbs: Breadcrumbs,
|
|
24
|
+
sidebar: window.top.testgen.components.Sidebar,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
if (Object.keys(componentById).includes(id)) {
|
|
28
|
+
return componentById[id](props);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return '';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
window.addEventListener('message', (event) => {
|
|
35
|
+
if (event.data.type === 'streamlit:render') {
|
|
36
|
+
const componentId = event.data.args.id;
|
|
37
|
+
const componentKey = event.data.args.key;
|
|
38
|
+
|
|
39
|
+
let van = currentWindowVan;
|
|
40
|
+
let mountPoint = document.body;
|
|
41
|
+
let componentState = window.testgen.states[componentKey];
|
|
42
|
+
if (shouldRenderOutsideFrame(componentId)) {
|
|
43
|
+
window.frameElement.style.display = 'none';
|
|
44
|
+
componentState = window.top.testgen.states[componentKey];
|
|
45
|
+
mountPoint = window.frameElement.parentElement;
|
|
46
|
+
van = topWindowVan;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (componentId === 'sidebar') {
|
|
50
|
+
window.top.testgen.components.Sidebar.onLogout = logout;
|
|
51
|
+
window.top.testgen.components.Sidebar.onProjectChanged = changeProject;
|
|
52
|
+
window.top.testgen.components.Sidebar.StreamlitInstance = Streamlit;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (componentState === undefined) {
|
|
56
|
+
document.body.dataset.component = event.data.args.id;
|
|
57
|
+
|
|
58
|
+
componentState = {};
|
|
59
|
+
for (const [ key, value ] of Object.entries(event.data.args.props)) {
|
|
60
|
+
componentState[key] = van.state(value);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (shouldRenderOutsideFrame(componentId)) {
|
|
64
|
+
window.top.testgen.states[componentKey] = componentState;
|
|
65
|
+
} else {
|
|
66
|
+
window.testgen.states[componentKey] = componentState;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return van.add(mountPoint, TestGenComponent(componentId, componentState));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const [ key, value ] of Object.entries(event.data.args.props)) {
|
|
73
|
+
if (componentState[key].val !== value) {
|
|
74
|
+
componentState[key].val = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
Streamlit.init();
|
|
81
|
+
|
|
82
|
+
function shouldRenderOutsideFrame(componentId) {
|
|
83
|
+
return 'sidebar' === componentId;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function logout(authCookieName) {
|
|
87
|
+
window.parent.postMessage({ type: 'TestgenLogout', cookie: authCookieName }, '*');
|
|
88
|
+
Streamlit.sendData({ logout: true });
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function changeProject(/** @type string */ projectCode) {
|
|
93
|
+
Streamlit.sendData({ change_to_project: projectCode });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
window.testgen = {
|
|
97
|
+
states: {},
|
|
98
|
+
loadedStylesheets: {},
|
|
99
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const Streamlit = {
|
|
2
|
+
init: () => {
|
|
3
|
+
sendMessageToStreamlit('streamlit:componentReady', { apiVersion: 1 });
|
|
4
|
+
},
|
|
5
|
+
setFrameHeight: (height) => {
|
|
6
|
+
sendMessageToStreamlit('streamlit:setFrameHeight', { height: height });
|
|
7
|
+
},
|
|
8
|
+
sendData: (data) => {
|
|
9
|
+
sendMessageToStreamlit('streamlit:setComponentValue', { value: data, dataType: 'json' });
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function sendMessageToStreamlit(type, data) {
|
|
14
|
+
if (window.top) {
|
|
15
|
+
window.top.postMessage(Object.assign({ type: type, isStreamlitMessage: true }, data), '*');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { Streamlit };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
let e,t,l,r,o,f=Object,n=f.getPrototypeOf,s=document,a={isConnected:1},i={},d=n(a),u=n(n),_=(e,t,l,r)=>(e??(setTimeout(l,r),new Set)).add(t),h=(e,t,r)=>{let o=l;l=t;try{return e(r)}catch(e){return console.error(e),r}finally{l=o}},c=e=>e.filter(e=>e.t?.isConnected),g=t=>o=_(o,t,()=>{for(let e of o)e.l=c(e.l),e.o=c(e.o);o=e},1e3),w={get val(){return l?.add(this),this.i},get oldVal(){return l?.add(this),this.u},set val(l){let r=this;if(l!==r.i){r.i=l;let o=[...r.o=c(r.o)];for(let t of o)x(t.f,t.s,t.t),t.t=e;r.l.length?t=_(t,r,p):r.u=l}}},v=e=>({__proto__:w,i:e,u:e,l:[],o:[]}),S=e=>n(e??0)===w,y=(e,t)=>{let l=new Set,o={f:e},f=r;r=[];let n=h(e,l,t);n=(n??s).nodeType?n:new Text(n);for(let e of l)g(e),e.l.push(o);for(let e of r)e.t=n;return r=f,o.t=n},x=(e,t=v(),l)=>{let o=new Set,f={f:e,s:t};f.t=l??r?.push(f)??a,t.val=h(e,o);for(let e of o)g(e),e.o.push(f);return t},V=(t,...l)=>{for(let r of l.flat(1/0)){let l=n(r??0),o=l===w?y(()=>r.val):l===u?y(r):r;o!=e&&t.append(o)}return t},b=t=>new Proxy((l,...r)=>{let[o,...a]=n(r[0]??0)===d?r:[{},...r],_=t?s.createElementNS(t,l):s.createElement(l);for(let[t,r]of f.entries(o)){let o=l=>l?f.getOwnPropertyDescriptor(l,t)??o(n(l)):e,s=l+","+t,a=i[s]??(i[s]=o(n(_))?.set??0),d=a?a.bind(_):_.setAttribute.bind(_,t),h=n(r??0);h===w?y(()=>(d(r.val),_)):h!==u||t.startsWith("on")&&!r.h?d(r):y(()=>(d(r()),_))}return V(_,...a)},{get:(t,l)=>t.bind(e,l)}),m=(e,t)=>t?t!==e&&e.replaceWith(t):e.remove(),p=()=>{let l=[...t].filter(e=>e.i!==e.u);t=e;for(let t of new Set(l.flatMap(e=>e.l=c(e.l))))m(t.t,y(t.f,t.t)),t.t=e;for(let e of l)e.u=e.i};export default{add:V,_:e=>(e.h=1,e),tags:b(),tagsNS:b,state:v,val:e=>S(e)?e.val:e,oldVal:e=>S(e)?e.oldVal:e,derive:x,hydrate:(e,t)=>m(e,y(t,e))};
|
|
File without changes
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Streamlit component's callbacks are broken for CustomComponents, this is
|
|
3
|
+
a temporary patch picked up from the internet.
|
|
4
|
+
|
|
5
|
+
https://gist.github.com/okld/1a2b2fd2cb9f85fc8c4e92e26c6597d5
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from streamlit import session_state
|
|
11
|
+
from streamlit.components.v1 import components
|
|
12
|
+
|
|
13
|
+
LOG = logging.getLogger("testgen")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _patch_register_widget(register_widget):
|
|
17
|
+
def wrapper_register_widget(*args, **kwargs):
|
|
18
|
+
user_key = kwargs.get("user_key", None)
|
|
19
|
+
callbacks = session_state.get("_components_callbacks", None)
|
|
20
|
+
|
|
21
|
+
# Check if a callback was registered for that user_key.
|
|
22
|
+
if user_key and callbacks and user_key in callbacks:
|
|
23
|
+
callback = callbacks[user_key]
|
|
24
|
+
|
|
25
|
+
# Add callback-specific args for the real register_widget function.
|
|
26
|
+
kwargs["on_change_handler"] = callback[0]
|
|
27
|
+
kwargs["args"] = callback[1]
|
|
28
|
+
kwargs["kwargs"] = callback[2]
|
|
29
|
+
|
|
30
|
+
# Call the original function with updated kwargs.
|
|
31
|
+
return register_widget(*args, **kwargs)
|
|
32
|
+
|
|
33
|
+
return wrapper_register_widget
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Patch function only once.
|
|
37
|
+
if not hasattr(components.register_widget, "__callbacks_patched__"):
|
|
38
|
+
components.register_widget.__callbacks_patched__ = True
|
|
39
|
+
components.register_widget = _patch_register_widget(components.register_widget)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def register_callback(element_key, callback, *callback_args, **callback_kwargs):
|
|
43
|
+
# Initialize callbacks store.
|
|
44
|
+
if "_components_callbacks" not in session_state:
|
|
45
|
+
session_state._components_callbacks = {}
|
|
46
|
+
|
|
47
|
+
# Register a callback for a given element_key.
|
|
48
|
+
try:
|
|
49
|
+
session_state._components_callbacks[element_key] = (callback, callback_args, callback_kwargs)
|
|
50
|
+
except:
|
|
51
|
+
LOG.debug("unexpected error registering component callback", exc_info=False, stack_info=False)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
from streamlit.components import v1 as components
|
|
4
|
+
|
|
5
|
+
components_dir = pathlib.Path(__file__).parent.parent.joinpath("frontend")
|
|
6
|
+
component_function = components.declare_component("testgen", path=components_dir)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def component(*, id_, props, key=None, default=None):
|
|
10
|
+
component_props = props
|
|
11
|
+
if not component_props:
|
|
12
|
+
component_props = {}
|
|
13
|
+
return component_function(id=id_, props=component_props, key=key, default=default)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# ruff: noqa: F401
|
|
2
|
+
|
|
3
|
+
from testgen.ui.components.widgets.breadcrumbs import breadcrumbs
|
|
4
|
+
from testgen.ui.components.widgets.location import LocationChanged, location
|
|
5
|
+
from testgen.ui.components.widgets.modal import Modal
|
|
6
|
+
from testgen.ui.components.widgets.sidebar import sidebar
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
from testgen.ui.components.utils.component import component
|
|
5
|
+
|
|
6
|
+
LOG = logging.getLogger("testgen")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def breadcrumbs(
|
|
10
|
+
key: str = "testgen:breadcrumbs",
|
|
11
|
+
breadcrumbs: list["Breadcrumb"] | None = None,
|
|
12
|
+
) -> None:
|
|
13
|
+
"""
|
|
14
|
+
Testgen component to display the breadcrumbs with a hash link on
|
|
15
|
+
each page.
|
|
16
|
+
|
|
17
|
+
# Parameters
|
|
18
|
+
:param key: unique key to give the component a persisting state
|
|
19
|
+
:param breadcrumbs: list of dicts with label and path
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
component(
|
|
23
|
+
id_="breadcrumbs",
|
|
24
|
+
key=key,
|
|
25
|
+
default={},
|
|
26
|
+
props={"breadcrumbs": breadcrumbs},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Breadcrumb(typing.TypedDict):
|
|
31
|
+
path: str | None
|
|
32
|
+
label: str
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import dataclasses
|
|
3
|
+
import logging
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
import streamlit as st
|
|
7
|
+
|
|
8
|
+
from testgen.ui.components.utils.callbacks import register_callback
|
|
9
|
+
from testgen.ui.components.utils.component import component
|
|
10
|
+
from testgen.ui.session import session
|
|
11
|
+
|
|
12
|
+
LOG = logging.getLogger("testgen")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def location(
|
|
16
|
+
key: str = "testgen:location",
|
|
17
|
+
on_change: typing.Callable[["LocationChanged"], None] | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Testgen component to listen for location changes in the url hash.
|
|
21
|
+
|
|
22
|
+
# Parameters
|
|
23
|
+
:param key: unique key to give the component a persisting state
|
|
24
|
+
:param on_change: callback for when the browser location changes
|
|
25
|
+
"""
|
|
26
|
+
register_callback(key, _handle_location_change, key, on_change)
|
|
27
|
+
|
|
28
|
+
initialized = bool(session.renders and session.renders > 1)
|
|
29
|
+
current_page_code = _encode_page(session.current_page, session.current_page_args or {})
|
|
30
|
+
|
|
31
|
+
change = component(
|
|
32
|
+
id_="location",
|
|
33
|
+
key=key,
|
|
34
|
+
default={},
|
|
35
|
+
props={"initialized": initialized, "current_page_code": current_page_code},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if not initialized and change:
|
|
39
|
+
change = LocationChanged(**change)
|
|
40
|
+
if _encode_page(change.page, change.args) != current_page_code:
|
|
41
|
+
_handle_location_change(key, on_change)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _handle_location_change(key: str, callback: typing.Callable[["LocationChanged"], None] | None):
|
|
45
|
+
if callback:
|
|
46
|
+
change = st.session_state[key]
|
|
47
|
+
if "page" not in change:
|
|
48
|
+
change["page"] = "overview"
|
|
49
|
+
return callback(LocationChanged(**change))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _encode_page(page: str, args: dict) -> str | None:
|
|
53
|
+
page_code = None
|
|
54
|
+
if page:
|
|
55
|
+
query_params = "&".join([f"{name}={value}" for name, value in args.items()])
|
|
56
|
+
if query_params:
|
|
57
|
+
page = f"{page}?{query_params}"
|
|
58
|
+
page_code = base64.b64encode(page.encode()).decode()
|
|
59
|
+
return page_code
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclasses.dataclass
|
|
63
|
+
class LocationChanged:
|
|
64
|
+
page: str
|
|
65
|
+
args: dict = dataclasses.field(default_factory=dict)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
|
|
3
|
+
import streamlit
|
|
4
|
+
import streamlit.components.v1 as components
|
|
5
|
+
from streamlit_modal import Modal as BaseModal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Modal(BaseModal):
|
|
9
|
+
@contextmanager
|
|
10
|
+
def container(self):
|
|
11
|
+
streamlit.markdown(self._modal_styles(), unsafe_allow_html=True)
|
|
12
|
+
with streamlit.container():
|
|
13
|
+
_container = streamlit.container()
|
|
14
|
+
if self.title:
|
|
15
|
+
_container.markdown(f"<h2>{self.title}</h2>", unsafe_allow_html=True)
|
|
16
|
+
|
|
17
|
+
close_ = streamlit.button("X", key=f"{self.key}-close")
|
|
18
|
+
if close_:
|
|
19
|
+
self.close()
|
|
20
|
+
|
|
21
|
+
if not close_:
|
|
22
|
+
components.html(self._modal_script(), height=0, width=0)
|
|
23
|
+
|
|
24
|
+
with _container:
|
|
25
|
+
yield _container
|
|
26
|
+
|
|
27
|
+
def _modal_styles(self) -> str:
|
|
28
|
+
max_width = f"{self.max_width}px" if self.max_width else "unset"
|
|
29
|
+
|
|
30
|
+
return f"""
|
|
31
|
+
<style>
|
|
32
|
+
div[data-modal-container='true'][key='{self.key}'] {{
|
|
33
|
+
position: fixed;
|
|
34
|
+
width: 100vw !important;
|
|
35
|
+
left: 0;
|
|
36
|
+
z-index: 1001;
|
|
37
|
+
}}
|
|
38
|
+
|
|
39
|
+
div[data-modal-container='true'][key='{self.key}'] > div:first-child {{
|
|
40
|
+
margin: auto;
|
|
41
|
+
}}
|
|
42
|
+
|
|
43
|
+
div[data-modal-container='true'][key='{self.key}'] h1 a {{
|
|
44
|
+
display: none
|
|
45
|
+
}}
|
|
46
|
+
|
|
47
|
+
div[data-modal-container='true'][key='{self.key}']::before {{
|
|
48
|
+
position: fixed;
|
|
49
|
+
content: ' ';
|
|
50
|
+
left: 0;
|
|
51
|
+
right: 0;
|
|
52
|
+
top: 0;
|
|
53
|
+
bottom: 0;
|
|
54
|
+
z-index: 1000;
|
|
55
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
56
|
+
}}
|
|
57
|
+
div[data-modal-container='true'][key='{self.key}'] > div:first-child {{
|
|
58
|
+
max-width: {max_width};
|
|
59
|
+
}}
|
|
60
|
+
|
|
61
|
+
div[data-modal-container='true'][key='{self.key}'] > div:first-child > div:first-child {{
|
|
62
|
+
width: unset !important;
|
|
63
|
+
background-color: #fff;
|
|
64
|
+
padding: {self.padding}px;
|
|
65
|
+
margin-top: {2*self.padding}px;
|
|
66
|
+
margin-left: -{self.padding}px;
|
|
67
|
+
margin-right: -{self.padding}px;
|
|
68
|
+
margin-bottom: -{2*self.padding}px;
|
|
69
|
+
z-index: 1001;
|
|
70
|
+
border-radius: 5px;
|
|
71
|
+
}}
|
|
72
|
+
div[data-modal-container='true'][key='{self.key}'] > div > div:nth-child(1 of .element-container) {{
|
|
73
|
+
z-index: 1003;
|
|
74
|
+
position: absolute;
|
|
75
|
+
}}
|
|
76
|
+
div[data-modal-container='true'][key='{self.key}'] > div > div:nth-child(1 of .element-container) > div {{
|
|
77
|
+
text-align: right;
|
|
78
|
+
padding-right: {self.padding}px;
|
|
79
|
+
max-width: {max_width};
|
|
80
|
+
}}
|
|
81
|
+
|
|
82
|
+
div[data-modal-container='true'][key='{self.key}'] > div > div:nth-child(1 of .element-container) > div > button {{
|
|
83
|
+
right: 0;
|
|
84
|
+
margin-top: {2*self.padding + 14}px;
|
|
85
|
+
}}
|
|
86
|
+
</style>
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def _modal_script(self) -> str:
|
|
90
|
+
return f"""
|
|
91
|
+
<script type="text/javascript">
|
|
92
|
+
const modalContainer = window.frameElement.parentElement.parentElement.parentElement;
|
|
93
|
+
|
|
94
|
+
modalContainer.setAttribute('data-modal-container', 'true');
|
|
95
|
+
modalContainer.setAttribute('key', '{self.key}');
|
|
96
|
+
</script>
|
|
97
|
+
"""
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import logging
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
import streamlit as st
|
|
6
|
+
|
|
7
|
+
from testgen.ui.components.utils.callbacks import register_callback
|
|
8
|
+
from testgen.ui.components.utils.component import component
|
|
9
|
+
from testgen.ui.navigation.menu import Menu
|
|
10
|
+
from testgen.ui.services import authentication_service
|
|
11
|
+
|
|
12
|
+
LOG = logging.getLogger("testgen")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def sidebar(
|
|
16
|
+
key: str = "testgen:sidebar",
|
|
17
|
+
username: str | None = None,
|
|
18
|
+
menu: Menu = None,
|
|
19
|
+
current_page: str | None = None,
|
|
20
|
+
current_project: str | None = None,
|
|
21
|
+
on_logout: typing.Callable[[], None] | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Testgen custom component to display a styled menu over streamlit's
|
|
25
|
+
sidebar.
|
|
26
|
+
|
|
27
|
+
# Parameters
|
|
28
|
+
:param key: unique key to give the component a persisting state
|
|
29
|
+
:param username: username to display at the bottom of the menu
|
|
30
|
+
:param menu: menu object with all root pages
|
|
31
|
+
:param current_page: page address to highlight the selected item
|
|
32
|
+
:param on_logout: callback for when user clicks logout
|
|
33
|
+
:param on_project_changed: callback for when user switches projects
|
|
34
|
+
"""
|
|
35
|
+
register_callback(key, _handle_callbacks, key, on_logout)
|
|
36
|
+
|
|
37
|
+
component(
|
|
38
|
+
id_="sidebar",
|
|
39
|
+
props={
|
|
40
|
+
"username": username,
|
|
41
|
+
"menu": menu.filter_for_current_user().sort_items().asdict(),
|
|
42
|
+
"current_page": current_page,
|
|
43
|
+
"current_project": current_project,
|
|
44
|
+
"auth_cookie_name": authentication_service.AUTH_TOKEN_COOKIE_NAME,
|
|
45
|
+
},
|
|
46
|
+
key=key,
|
|
47
|
+
default={},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _handle_callbacks(
|
|
52
|
+
key: str,
|
|
53
|
+
on_logout: typing.Callable[[], None] | None = None,
|
|
54
|
+
):
|
|
55
|
+
action = st.session_state[key]
|
|
56
|
+
action = MenuAction(**action)
|
|
57
|
+
|
|
58
|
+
if action.logout and on_logout:
|
|
59
|
+
return on_logout()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Project(typing.TypedDict):
|
|
63
|
+
code: str
|
|
64
|
+
name: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclasses.dataclass
|
|
68
|
+
class MenuAction:
|
|
69
|
+
logout: bool | None = None
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
|
|
3
|
+
from testgen.ui.services import authentication_service
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclasses.dataclass
|
|
7
|
+
class MenuItem:
|
|
8
|
+
icon: str
|
|
9
|
+
label: str
|
|
10
|
+
page: str | None = dataclasses.field(default=None)
|
|
11
|
+
roles: list[authentication_service.RoleType] | None = dataclasses.field(default_factory=list)
|
|
12
|
+
order: int = dataclasses.field(default=0)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclasses.dataclass
|
|
16
|
+
class Version:
|
|
17
|
+
current: str
|
|
18
|
+
latest: str
|
|
19
|
+
schema: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclasses.dataclass
|
|
23
|
+
class Menu:
|
|
24
|
+
items: list[MenuItem]
|
|
25
|
+
version: Version
|
|
26
|
+
|
|
27
|
+
def filter_for_current_user(self) -> "Menu":
|
|
28
|
+
filtered_items = []
|
|
29
|
+
for menu_item in self.items:
|
|
30
|
+
item_roles = menu_item.roles or []
|
|
31
|
+
if len(item_roles) <= 0 or any(map(authentication_service.current_user_has_role, item_roles)):
|
|
32
|
+
filtered_items.append(menu_item)
|
|
33
|
+
return dataclasses.replace(self, items=filtered_items)
|
|
34
|
+
|
|
35
|
+
def sort_items(self) -> "Menu":
|
|
36
|
+
return dataclasses.replace(self, items=sorted(self.items, key=lambda item: item.order))
|
|
37
|
+
|
|
38
|
+
def update_version(self, version: Version) -> "Menu":
|
|
39
|
+
return dataclasses.replace(self, version=version)
|
|
40
|
+
|
|
41
|
+
def asdict(self):
|
|
42
|
+
return dataclasses.asdict(self)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import testgen.ui.navigation.router
|
|
5
|
+
from testgen.ui.navigation.menu import MenuItem
|
|
6
|
+
|
|
7
|
+
CanActivateGuard = typing.Callable[[], bool | str]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Page(abc.ABC):
|
|
11
|
+
path: str
|
|
12
|
+
menu_item: MenuItem | None = None
|
|
13
|
+
can_activate: typing.ClassVar[list[CanActivateGuard] | None] = None
|
|
14
|
+
|
|
15
|
+
def __init__(self, router: testgen.ui.navigation.router.Router) -> None:
|
|
16
|
+
self.router = router
|
|
17
|
+
|
|
18
|
+
@abc.abstractmethod
|
|
19
|
+
def render(self, **kwargs) -> None:
|
|
20
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import typing
|
|
5
|
+
|
|
6
|
+
import streamlit as st
|
|
7
|
+
|
|
8
|
+
import testgen.ui.navigation.page
|
|
9
|
+
from testgen.utils.singleton import Singleton
|
|
10
|
+
|
|
11
|
+
CanActivateGuard = typing.Callable[[], bool | str]
|
|
12
|
+
|
|
13
|
+
LOG = logging.getLogger("testgen")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Router(Singleton):
|
|
17
|
+
active: testgen.ui.navigation.page.Page | None
|
|
18
|
+
_default: type[testgen.ui.navigation.page.Page] | None
|
|
19
|
+
_routes: dict[str, type[testgen.ui.navigation.page.Page]]
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
/,
|
|
24
|
+
routes: list[type[testgen.ui.navigation.page.Page]],
|
|
25
|
+
default: type[testgen.ui.navigation.page.Page] | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self._routes = {}
|
|
28
|
+
|
|
29
|
+
for route in routes:
|
|
30
|
+
self._routes[route.path] = route
|
|
31
|
+
|
|
32
|
+
self.active = None
|
|
33
|
+
self._default = default
|
|
34
|
+
if self._default:
|
|
35
|
+
self._routes[self._default.path] = self._default
|
|
36
|
+
|
|
37
|
+
def navigate(self, /, to: str, with_args: dict | None = None) -> None:
|
|
38
|
+
try:
|
|
39
|
+
route = self._routes[to]
|
|
40
|
+
|
|
41
|
+
bc_source = route(self).path
|
|
42
|
+
|
|
43
|
+
for guard in route.can_activate or []:
|
|
44
|
+
can_activate = guard()
|
|
45
|
+
if type(can_activate) == str:
|
|
46
|
+
return self.navigate(to=can_activate, with_args={})
|
|
47
|
+
|
|
48
|
+
if not can_activate and self._default:
|
|
49
|
+
return self.navigate(to=self._default.path, with_args=with_args)
|
|
50
|
+
|
|
51
|
+
if not isinstance(self.active, route):
|
|
52
|
+
self.active = route(self)
|
|
53
|
+
|
|
54
|
+
self.active.render(**(with_args or {}))
|
|
55
|
+
except KeyError as k:
|
|
56
|
+
error_message = f"{bc_source}: {k!s}"
|
|
57
|
+
st.error(error_message)
|
|
58
|
+
LOG.exception(error_message)
|
|
59
|
+
return self.navigate(to=self._default.path, with_args=with_args)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
error_message = f"{bc_source}: {e!s}"
|
|
62
|
+
st.error(error_message)
|
|
63
|
+
LOG.exception(error_message)
|
|
File without changes
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import streamlit as st
|
|
2
|
+
|
|
3
|
+
import testgen.ui.services.database_service as db
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@st.cache_data(show_spinner=False)
|
|
7
|
+
def get_users(schema):
|
|
8
|
+
sql = f"""SELECT
|
|
9
|
+
id::VARCHAR(50),
|
|
10
|
+
username, email, "name", "password", preauthorized, role
|
|
11
|
+
FROM {schema}.auth_users"""
|
|
12
|
+
return db.retrieve_data(sql)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def delete_users(schema, user_ids):
|
|
16
|
+
if user_ids is None or len(user_ids) == 0:
|
|
17
|
+
raise ValueError("No user is specified.")
|
|
18
|
+
|
|
19
|
+
items = [f"'{item}'" for item in user_ids]
|
|
20
|
+
sql = f"""DELETE FROM {schema}.auth_users WHERE id in ({",".join(items)})"""
|
|
21
|
+
db.execute_sql(sql)
|
|
22
|
+
st.cache_data.clear()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def add_user(schema, user, encrypted_password):
|
|
26
|
+
sql = f"""INSERT INTO {schema}.auth_users
|
|
27
|
+
(username, email, name, password, role)
|
|
28
|
+
SELECT
|
|
29
|
+
'{user["username"]}' as username,
|
|
30
|
+
'{user["email"]}' as email,
|
|
31
|
+
'{user["name"]}' as name,
|
|
32
|
+
'{encrypted_password}' as password,
|
|
33
|
+
'{user["role"]}' as role;"""
|
|
34
|
+
db.execute_sql(sql)
|
|
35
|
+
st.cache_data.clear()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def edit_user(schema, user, encrypted_password):
|
|
39
|
+
sql = f"""UPDATE {schema}.auth_users SET
|
|
40
|
+
username = '{user["username"]}',
|
|
41
|
+
email = '{user["email"]}',
|
|
42
|
+
name = '{user["name"]}',
|
|
43
|
+
password = '{encrypted_password}',
|
|
44
|
+
role = '{user["role"]}'
|
|
45
|
+
WHERE id = '{user["user_id"]}';"""
|
|
46
|
+
db.execute_sql(sql)
|
|
47
|
+
st.cache_data.clear()
|