diff --git a/docs/developer_guide_pyegeria_textual.md b/docs/developer_guide_pyegeria_textual.md index 362462f2..6ae266a5 100644 --- a/docs/developer_guide_pyegeria_textual.md +++ b/docs/developer_guide_pyegeria_textual.md @@ -686,7 +686,7 @@ That is the second of the two standard worker patterns, so you now have both: ### 7.5 Acting on the selected row My Profile lets the user act on "the selected row" from several places: add a comment -(Ctrl+A), show comments (Ctrl+S), edit (Ctrl+T), bookmark it (Ctrl+K) and open bookmarks with +(Ctrl+N), show comments (Ctrl+O), edit (Ctrl+T), bookmark it (Ctrl+K) and open bookmarks with its GUID ready to add (Ctrl+B). Two problems need solving. **Which row is selected?** `MainScreen` records the table and row whenever the user @@ -737,7 +737,7 @@ qualified name. Some tables, such as Shop for Data's glossary table (built from Because `get_row_guid` takes the table itself, not a table name on one particular screen, any screen can use it, and each key binding is a few lines: -- **Ctrl+K** on the main screen, or **k** on Shop for Data, calls +- **Ctrl+K**, on the main screen or Shop for Data, calls `self.app.bookmark_table_row(table, row_key)`, which bookmarks the row straight away. - **Ctrl+B** calls `self.app.show_my_bookmarks(self.app.get_row_guid(table, row_key))`. The bookmark screen receives the GUID as `target_guid` and pre-fills its input with it, just as diff --git a/docs/my_profile_app_manual.md b/docs/my_profile_app_manual.md index 1065387c..a5f2187e 100644 --- a/docs/my_profile_app_manual.md +++ b/docs/my_profile_app_manual.md @@ -126,7 +126,7 @@ The header shows your full name, user ID and **Karma Points** (your contribution | **User Associations** | Status or Type, Name, Description, GUID | The communities you belong to | | **My Collections** | Collection Name, Collection Description, Collection GUID | Collections you created (your bookmarks collection is not listed here) | | **Other Functions** | – | A menu of further features ([below](#other-functions-menu)) | -| **Roles** | Role Name, Role Type, Description, GUID | Roles you have been appointed to | +| **Roles** | Role Name, Role Type, Description, GUID | Roles you have been appointed to. Press `Enter` on a TeamLeader or TeamMember role (or click it a second time) to see the other people in that team | | **Teams** | Assignment Type, Team Name, Description, GUID | Teams you are a member of | | **Blogs** | Blog Title, Date, Text, GUID | Your blog entries | | **Journal** | Journal Entry, Date, Text, GUID | Your journal entries | @@ -134,18 +134,20 @@ The header shows your full name, user ID and **Karma Points** (your contribution | **User Identity** | Display Name, User ID, Distinguished Name, GUID | User identities linked to your profile | | **Projects** | Status or Type, Name, Description, GUID | Projects you are involved in | -Click or move into a table to select it; the highlighted row is the "selected row" used by the shortcuts below. Press `r` to reload everything from Egeria. The app also reloads automatically when you close an Add screen. +Click or move into a table to select it; the highlighted row is the "selected row" used by the shortcuts below. Press `ctrl+r` to reload everything from Egeria. The app also reloads automatically when you close an Add screen. + +**Seeing your team.** Selecting a TeamLeader or TeamMember role in the Roles table (for example `TeamLeader::Department::0010`) opens the **My Team** screen, which lists the team's members and their assignment types; press `Escape` to return. Selecting any other role shows a warning that it is not a team role. If Egeria returns no members for the team, or the lookup fails, a message says so. Main screen shortcuts: | Key | Action | | :--- | :--- | | `ctrl+t` | **Edit Selected Table** – opens the edit screen for the selected table, where you add and delete rows | -| `ctrl+a` | **Add Comment** to the selected row | -| `ctrl+s` | **Show Comments** for the selected row | -| `ctrl+b` | **Manage Bookmarks** | +| `ctrl+o` | **Show Comments** for the selected row | +| `ctrl+n` | **Add Comment** to the selected row | | `ctrl+k` | **Bookmark** the element in the selected row | -| `r` | Refresh all data from Egeria | +| `ctrl+b` | **Manage Bookmarks** | +| `ctrl+r` | Refresh all data from Egeria | | `ctrl+f` | Leave feedback (works on every screen) | | `q` | Quit the app | @@ -155,8 +157,8 @@ Adding and deleting both go through the table's **edit screen**: 1. On the main screen, select a table and press `ctrl+t`. 2. The edit screen shows a copy of that table. Then: - - `a` – **Add Row**: opens the Add screen for that kind of element. - - `d` – **Delete Row**: asks you to confirm, then deletes the highlighted element from Egeria. The row is removed only if Egeria accepts the delete. You can delete one row at a time. + - `ctrl+n` – **Add Row**: opens the Add screen for that kind of element. + - `ctrl+d` – **Delete Row**: asks you to confirm, then deletes the highlighted element from Egeria. The row is removed only if Egeria accepts the delete. You can delete one row at a time. - `Escape` – back to the main screen. Edit screens add and delete; they do not edit a row's values in place. To change your own details use **Edit Profile** from the Other Functions menu. @@ -165,10 +167,10 @@ Edit screens add and delete; they do not edit a row's values in place. To change Every Add screen works the same way: - Fill in the fields. Those marked \* are required. -- Press **Add** (or `ctrl+a`). +- Press **Add** (or `ctrl+s`). - Some screens have a switch that links the new element to you. It is on by default. -If a required field is missing or a value is invalid, the screen tells you which one. After a successful add the form is cleared, so you can add another; after a failed add your input is kept so you can correct it. Press **Quit** (or `q` outside a text field) to close the screen. You go back to the main screen and the data is reloaded. +If a required field is missing or a value is invalid, the screen tells you which one. After a successful add the form is cleared, so you can add another; after a failed add your input is kept so you can correct it. Press **Quit** (or `Escape`) to close the screen. You go back to the main screen and the data is reloaded. | Table | Fields | Link switch | Egeria result | | :--- | :--- | :--- | :--- | @@ -187,15 +189,15 @@ If a required field is missing or a value is invalid, the screen tells you which Comments can be attached to any element in a main-screen table, and responses can be attached to comments to any depth. -**Add a comment from the main screen:** select a row, press `ctrl+a`, enter the comment and a comment type (`Question`, `Answer`, `Suggestion` or `Requirement`), then press **Submit**. +**Add a comment from the main screen:** select a row, press `ctrl+n`, enter the comment and a comment type (`Question`, `Answer`, `Suggestion` or `Requirement`), then press **Submit** (`ctrl+s`). -**View and reply:** select a row and press `ctrl+s`. The comments screen lists the comments on that element. +**View and reply:** select a row and press `ctrl+o`. The comments screen lists the comments on that element. -- `ctrl+a` adds a new comment to the element being shown. -- `ctrl+r` on a highlighted comment re-opens the screen focused on *that comment*, showing its responses. Here `ctrl+a` adds a response. Repeat to go deeper into a thread. -- `q` closes the screen. +- `ctrl+n` adds a new comment to the element being shown. +- `ctrl+o` on a highlighted comment re-opens the screen focused on *that comment*, showing its responses. Here `ctrl+n` adds a response. Repeat to go deeper into a thread. +- `Escape` or `q` closes the screen. -If an element has no comments yet, the screen tells you to use `ctrl+a`. +If an element has no comments yet, the screen tells you to use `ctrl+n`. ## Leaving Feedback @@ -214,21 +216,21 @@ A read-only table of your user identities: display name, category, description, Opens a screen with five tables, each loading in the background: **Glossaries**, **Digital Product Catalog**, **Data Dictionaries**, **Business Domains** and **Root Collections**. - **Enter** (or click) on a row opens an **overview** screen: a tree on the left and details on the right. Selecting a tree node shows its details – glossary terms, digital products (including sample data), dictionaries, business capabilities or collections. -- `s` – show sample data for the highlighted row. -- `u` or `ctrl+s` – subscribe to the highlighted item. -- `t` – search glossary terms. Back from the search (`g`) returns to Shop for Data. -- `k` – bookmark the highlighted item; `ctrl+b` – open bookmarks with its GUID ready to add. -- `b` or `Escape` – back to the main screen; `q` – quit the screen. +- `ctrl+l` – show sample data for the highlighted row. +- `ctrl+u` – subscribe to the highlighted item. +- `ctrl+g` – search glossary terms. `Escape` from the search returns to Shop for Data. +- `ctrl+k` – bookmark the highlighted item; `ctrl+b` – open bookmarks with its GUID ready to add. +- `Escape` or `q` – back to the main screen. -On the overview screen, `s` subscribes to the selected node, `b` goes back and `q` returns to the main screen. On a sample-data view, `s` subscribes, and `b`, `Escape` or `Enter` go back. +On the overview screen, `ctrl+u` subscribes to the selected node, `Escape` goes back and `q` returns to the main screen. On a sample-data view, `ctrl+u` subscribes, `Escape` goes back to Shop for Data and `q` returns to the main screen. -**Subscribing**, from any of these screens, opens the **Create Subscription Request** form. It has Display Name, Description, Identifier and Status (`DRAFT`, `PROPOSED` or `ACTIVE`; an invalid value becomes `DRAFT`). Press `c` to create the digital subscription in Egeria. The subscription is linked to the item you chose and to you as its subscriber. If either link can't be made, a warning says so; the subscription itself is still created. +**Subscribing**, from any of these screens, opens the **Create Subscription Request** form. It has Display Name, Description, Identifier and Status (`DRAFT`, `PROPOSED` or `ACTIVE`; an invalid value becomes `DRAFT`). Press `ctrl+s` to create the digital subscription in Egeria (`Escape` cancels). The subscription is linked to the item you chose and to you as its subscriber. If either link can't be made, a warning says so; the subscription itself is still created. ### Edit Profile -A form pre-filled with your profile details (courtesy title, job title, given names, surname, preferred name, pronouns, description, time zone, employee ID, preferred language, resident country). Press **Edit Profile** to save. The screen also has shortcuts that open the Communities (`ctrl+c`), Identities (`ctrl+i`), Roles (`ctrl+r`) and Teams (`ctrl+t`) edit screens. Press `q` to return. +A form pre-filled with your profile details (courtesy title, job title, given names, surname, preferred name, pronouns, description, time zone, employee ID, preferred language, resident country). Press **Edit Profile** (or `ctrl+s`) to save. The screen also has shortcuts that open the Communities (`ctrl+g`), Identities (`ctrl+l`), Roles (`ctrl+r`) and Teams (`ctrl+t`) edit screens. Press `Escape` to return. ### Subscriptions -A table of the digital subscriptions you created: name, status, description and GUID. Press `q` to return. +A table of the digital subscriptions you created: name, status, description and GUID. Press `Escape` or `q` to return. ### Technology Types Browse the technology type hierarchy as a tree. Selecting a type shows its **catalog templates** and **governance action processes**: @@ -236,27 +238,27 @@ Browse the technology type hierarchy as a tree. Selecting a type shows its **cat - **Templates:** pick one and press **Select Template**. Fill in its placeholder values and submit. Egeria creates a new element from the template, and a **status screen** shows the result. - **Processes:** pick one and press **Select Process**, then fill in its request parameters and submit. Only the parameters you fill in are sent. Egeria starts the governance action process, and a **status screen** shows its GUID. -On the status screen, `c` copies the GUID shown to your clipboard, and `Enter`/`q` closes it and returns you to the main screen. +On the status screen, `ctrl+y` copies the GUID shown to your clipboard, and `Enter`, `Escape` or `q` closes it and returns you to the main screen. ### User Bookmarks Opens the bookmarks screen (also `ctrl+b` on the main screen and on Shop for Data). It lists your bookmarks (name, type, GUID). -The easiest way to add a bookmark is from a table: select a row and press `ctrl+k` on the main screen, or highlight a row and press `k` on Shop for Data. Any row that has a GUID can be bookmarked, and so can a glossary row, which is identified by its qualified name. Bookmarking something that is already bookmarked just tells you so. +The easiest way to add a bookmark is from a table: select a row and press `ctrl+k` on the main screen, or highlight a row and press `ctrl+k` on Shop for Data. Any row that has a GUID can be bookmarked, and so can a glossary row, which is identified by its qualified name. Bookmarking something that is already bookmarked just tells you so. On the bookmarks screen: -- `ctrl+n` shows a field for the GUID of an element to bookmark. If you opened the screen with `ctrl+b` while a row was highlighted, that row's GUID is already filled in; otherwise copy a GUID from any table, or with `c` on a status screen. -- `d` removes the highlighted bookmark (the element itself is not deleted). -- `q` returns. +- `ctrl+n` shows a field for the GUID of an element to bookmark. If you opened the screen with `ctrl+b` while a row was highlighted, that row's GUID is already filled in; otherwise copy a GUID from any table, or with `ctrl+y` on a status screen. +- `ctrl+d` removes the highlighted bookmark (the element itself is not deleted). +- `Escape` or `q` returns. Your bookmarks are kept in Egeria as the members of your private **My Bookmarks** collection (qualified name `Bookmarks::`), and only that collection. It is created the first time you bookmark something, anchored to your profile and linked from it with a `ResourceList` relationship whose resource use is "Bookmarks". ### My Network Your network is the people linked to your profile in Egeria. The screen lists them (qualified name, display name, full name, pronouns, GUID and the description you gave the link). If your profile has not finished loading, the app tells you to wait and does not open the screen. -- **View details:** highlight a person and press `v` (or **Person Details**). A panel shows their **contact methods** and the **communities** they belong to. A message tells you if either is empty. +- **View details:** highlight a person and press `ctrl+o` (or **Person Details**). A panel shows their **contact methods** and the **communities** they belong to. A message tells you if either is empty. - **Add someone:** type all or part of a name in the search field and press `Enter`. Up to 10 matching people are listed, leaving out you and anyone already in your network. Select a person, optionally enter a **nickname** and a **description** of your relationship, then press **Add to Network**. **Cancel** clears the selection. -- **Remove someone:** highlight a person and press `d` (or **Remove from Network**), then confirm with **Remove**. **Cancel** or `Escape` keeps them. Only the link between you is removed; their profile is not changed. -- `q` returns to the main screen. +- **Remove someone:** highlight a person and press `ctrl+d` (or **Remove from Network**), then confirm with **Remove**. **Cancel** or `Escape` keeps them. Only the link between you is removed; their profile is not changed. +- `Escape` or `q` returns to the main screen. Each person in your network is linked to your profile with a `Peer` relationship. The nickname and description are stored on that relationship. @@ -265,31 +267,35 @@ See [Leaving Feedback](#leaving-feedback). ## Keyboard Reference -| Key | Where | Action | +Every screen follows the same rules: + +- `Escape` goes back one level, on every screen except the main screen. +- `q` returns to the main screen, or quits the app when you are on the main screen. Screens that are mostly text fields (Add screens, Add Comment, Create Subscription, Create/Edit Profile, Search for Term, Feedback, Template and Process forms) have no `q`, because it would be typed into the field; use `Escape`. +- Every other action is a `ctrl` key, and a key means the same thing wherever it appears. `ctrl` keys work even while a text field has focus. + +| Key | Action | Where | | :--- | :--- | :--- | -| `q` | Main screen | Quit the app | -| `r` | Main screen | Refresh all data from Egeria | -| `ctrl+f` | Any screen | Leave feedback | -| `F3` | Any screen (feedback owner only) | Show the feedback log | -| `ctrl+t` | Main screen | Edit the selected table | -| `ctrl+a` | Main screen | Comment on the selected row | -| `ctrl+s` | Main screen | Show comments for the selected row | -| `ctrl+b` | Main screen, Shop for Data | Bookmarks, with the highlighted row's GUID ready to add | -| `ctrl+k` | Main screen | Bookmark the selected row | -| `ctrl+n` / `d` / `q` | Bookmarks | Add by GUID / remove highlighted / close | -| `v` / `d` / `q` | My Network | Show highlighted person's details / remove highlighted person / close | -| `Enter` | My Network search field | Search for people to add | -| `a` / `d` / `Escape` | Edit screens | Add row / delete row / back | -| `ctrl+a` / `q` | Add screens | Add / close (outside a text field) | -| `ctrl+a` / `ctrl+r` / `q` | Comments screen | Add comment / show responses to highlighted comment / close | -| `s` / `u` or `ctrl+s` / `t` / `k` / `b` or `Escape` / `q` | Shop for Data | Sample / subscribe / search glossary terms / bookmark / back / close | -| `s` / `b` / `q` | Overview | Subscribe / back / back to main | -| `c` / `q` | Create Subscription | Create / cancel | -| `c` / `Enter` / `q` | Status screen | Copy GUID / close / close | -| `Escape` | Splash screen | Continue to the app | -| `ctrl+s` / `Escape` | Feedback | Submit / cancel | -| `Escape` | Confirm Delete | Cancel | -| `ctrl+c` | Anywhere | Force quit (terminal) | +| `Escape` | Back | Every screen except Main; continue on the splash screen; cancel on confirm dialogs | +| `q` | Main menu / quit the app | Every list screen / Main screen | +| `Enter` | Select the highlighted row; continue | Tables and trees; Status screen | +| `ctrl+s` | Save / submit the form | Add screens, Add Comment, Create Subscription, Create/Edit Profile, Feedback | +| `ctrl+n` | Add a new item | Edit screens (row), Bookmarks, Comments, Main screen (comment) | +| `ctrl+d` | Delete / remove the highlighted item | Edit screens, Bookmarks, My Network | +| `ctrl+o` | Open the highlighted item | Main screen (comments), Comments (responses), My Network (person details) | +| `ctrl+t` | Edit the selected table | Main screen | +| `ctrl+k` | Bookmark the highlighted row | Main screen, Shop for Data | +| `ctrl+b` | Bookmarks, with the highlighted row's GUID ready to add | Main screen, Shop for Data | +| `ctrl+u` | Subscribe | Shop for Data, Overview, sample-data view | +| `ctrl+l` | Show sample data | Shop for Data | +| `ctrl+g` | Search glossary terms | Shop for Data | +| `ctrl+e` | Expand / collapse the whole tree | Technology Types | +| `ctrl+y` | Copy the GUID shown | Status screen | +| `ctrl+x` | Close, flagging a bad result | Status screen | +| `ctrl+g` / `ctrl+l` / `ctrl+r` / `ctrl+t` | Edit Communities / Identities / Roles / Teams | Edit Profile | +| `ctrl+r` | Refresh all data from Egeria | Main screen | +| `ctrl+f` | Leave feedback | Any screen | +| `F3` | Show the feedback log | Any screen (feedback owner only) | +| `ctrl+c` | Force quit (terminal) | Anywhere | ## Exit and Return Codes @@ -309,7 +315,7 @@ When the app exits it returns a code; screens also use codes to talk to the app. | `412` / `413` | Error / empty result reloading a newly created profile | | `416` | Technology types could not be fetched | | `430` | Incompatible Python or pydantic version | -| `440` | Team, role or glossary lookup error | +| `440` | Glossary term lookup error | The full list is in `RETURN_CODES.md` in the app folder. @@ -322,9 +328,9 @@ The full list is in `RETURN_CODES.md` in the app folder. - the view server named by `EGERIA_VIEW_SERVER` is running; - `EGERIA_USER` and `EGERIA_USER_PASSWORD` are correct, and the user is allowed to use the view server. -**Something I added doesn't appear.** Press `r` on the main screen. Communities you create are not linked to you yet (see Known Limitations), so they don't appear in User Associations. +**Something I added doesn't appear.** Press `ctrl+r` on the main screen. Communities you create are not linked to you yet (see Known Limitations), so they don't appear in User Associations. -**The app quit unexpectedly.** The app only exits by itself in a few cases: a startup problem (codes `402`, `403`, `430`), a failure fetching technology types (`416`), or an error looking up a team (`440`). +**The app quit unexpectedly.** The app only exits by itself in a few cases: a startup problem (codes `402`, `403`, `430`), a failure fetching technology types (`416`), or an error looking up a glossary term (`440`). A failed team lookup is reported as a message instead. ## Known Limitations diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py index 085f6529..4b57e75a 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/AddCommentScreen.py @@ -18,9 +18,8 @@ class AddCommentScreen(ModalScreen): """ Add a comment to an Egeria Element the user has access to """ BINDINGS = [ - ("a", "add_comment", "Add Comment"), - ("c", "cancel", "Cancel"), - ("q", "quit", "Quit") + ("escape", "cancel", "Back"), + ("ctrl+s", "add_comment", "Add Comment"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py index f013534e..2da63d28 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/AddToElementsScreens.py @@ -30,8 +30,8 @@ class BaseAddScreen(ModalScreen): """ BINDINGS = [ - ("q", "quit", "Quit"), - ("ctrl+a", "add_element", "Add"), + ("escape", "quit", "Back"), + ("ctrl+s", "add_element", "Add"), ] CSS_PATH = "my_profile.tcss" @@ -202,7 +202,8 @@ class AddAssociationScreen(ModalScreen): """ BINDINGS = [ - ("q", "quit", "Quit"), + ("escape", "quit", "Back"), + ("q", "quit", "Main Menu"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py index 7408c676..673e68f4 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/CreateProfileScreen.py @@ -22,7 +22,8 @@ class CreateProfileScreen(ModalScreen[int]): 4xx on failure """ - BINDINGS = [("q", "dismiss(200)", "Quit")] + BINDINGS = [("escape", "quit", "Back"), + ("ctrl+s", "create_profile", "Create Profile")] CSS_PATH = "my_profile.tcss" def __init__(self, user: str | None = None, password: str | None = None, view_server: str | None = None, platform_url: str | None = None, *args, **kwargs): @@ -66,6 +67,10 @@ def compose(self) -> ComposeResult: yield Static() yield Footer() + def action_create_profile(self) -> None: + """ ctrl+s does the same as pressing the Create Profile button """ + self.create_profile() + @on(Button.Pressed, "#create_profile_btn") def create_profile(self) -> int: """Create a new profile in Egeria from data provided in Input fields.""" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py index 65304332..4f3ae925 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/CreateSubscriptionRequestScreen.py @@ -16,8 +16,8 @@ class CreateSubscriptionRequestScreen(ModalScreen): BINDINGS = [ - ("q", "quit", "Quit"), - ("c", "create_subscription", "Create Subscription") + ("escape", "quit", "Back"), + ("ctrl+s", "create_subscription", "Create Subscription"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py index d38c7701..7e65ece1 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/EditElementsScreens.py @@ -96,9 +96,10 @@ class BaseEditScreen(ModalScreen): SOURCE_TABLE: str = "" BINDINGS = [ - ("escape", "exit_screen", "Exit"), - ("a", "add_row", "Add Row"), - ("d", "delete_row", "Delete Row"), + ("escape", "exit_screen", "Back"), + ("q", "exit_screen", "Main Menu"), + ("ctrl+n", "add_row", "Add Row"), + ("ctrl+d", "delete_row", "Delete Row"), ] CSS_PATH = "my_profile.tcss" @@ -120,7 +121,7 @@ def compose(self) -> ComposeResult: yield Header(show_clock=True) yield Static(self.TITLE_TEXT, classes="span-3", id=self.TITLE_ID) yield Static( - " Press 'a' to add a row, 'd' to delete the selected row, Esc to exit." + " Press Ctrl+N to add a row, Ctrl+D to delete the selected row, Esc to go back." " Please note you may only delete one row at a time!", classes="span-3", ) @@ -351,11 +352,14 @@ class EditProfileScreen(ModalScreen[Any]): 4xx on failure """ - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("ctrl+c", "Edit_communities", "Edit communities"), - ("ctrl+i", "Edit_identities", "Edit identities"), - ("ctrl+r", "Edit_roles", "Edit roles"), - ("ctrl+t", "Edit_teams", "Edit teams"), + # ctrl+c and ctrl+i can't be used here: ctrl+c is Textual's copy/quit key and a + # terminal sends ctrl+i as Tab. Input swallows ctrl+a/d/e/k/u, so these avoid those too. + BINDINGS = [("escape", "quit", "Back"), + ("ctrl+s", "create_profile", "Save Profile"), + ("ctrl+g", "Edit_communities", "Edit Communities"), + ("ctrl+l", "Edit_identities", "Edit Identities"), + ("ctrl+r", "Edit_roles", "Edit Roles"), + ("ctrl+t", "Edit_teams", "Edit Teams"), ] CSS_PATH = "my_profile.tcss" @@ -470,6 +474,10 @@ def compose(self) -> ComposeResult: yield Static() yield Footer() + def action_create_profile(self) -> None: + """ ctrl+s does the same as pressing the Edit Profile button """ + self.create_profile() + @on(Button.Pressed, "#edit_profile_btn") def create_profile(self) -> Any: """Update profile in Egeria from data provided in Input fields.""" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py index 01479a62..1c12adb8 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/FeedbackScreens.py @@ -22,8 +22,8 @@ class FeedbackScreen(ModalScreen): """ Collect feedback about the screen the user was on when they pressed the feedback key """ BINDINGS = [ + ("escape", "cancel", "Back"), ("ctrl+s", "submit", "Submit Feedback"), - ("escape", "cancel", "Cancel"), ] CSS_PATH = "my_profile.tcss" @@ -98,8 +98,8 @@ class FeedbackLogScreen(ModalScreen): """ Show the accumulated entries in the private feedback note log """ BINDINGS = [ - ("q", "quit", "Quit"), ("escape", "quit", "Back"), + ("q", "quit", "Main Menu"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/GenericDataViewScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/GenericDataViewScreen.py index 95599ff2..453220c4 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/GenericDataViewScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/GenericDataViewScreen.py @@ -23,11 +23,9 @@ class GenericDataViewScreen(ModalScreen): """ BINDINGS = [ - ("q", "quit", "Quit"), - ("b", "back", "Go back"), ("escape", "back", "Back"), - ("return", "back", "Back"), - ("s", "subscribe", "Subscribe to Data Source"), + ("q", "quit", "Main Menu"), + ("ctrl+u", "subscribe", "Subscribe"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py index 9b183932..aff78958 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MainScreen.py @@ -13,17 +13,19 @@ from textual.widgets import DataTable, OptionList, Header, Static, Footer from textual.widgets._option_list import Option +from pyegeria import load_app_config, settings + class MainScreen(Screen): """Main Screen for My Profile App.""" BINDINGS = [ ("q", "app.quit", "Quit"), - ("ctrl+a", "add_comment","add comment to selected row"), - ("ctrl+b", "bookmarks", "Manage Bookmarks"), - ("ctrl+k", "bookmark_row", "Bookmark Selected Row"), - ("ctrl+s", "show_comments", "Show Comments for Selected Row"), ("ctrl+t", "edit_table", "Edit Selected Table"), + ("ctrl+o", "show_comments", "Show Comments"), + ("ctrl+n", "add_comment", "Add Comment"), + ("ctrl+k", "bookmark_row", "Bookmark Row"), + ("ctrl+b", "bookmarks", "Bookmarks"), ] CSS_PATH = "my_profile.tcss" @@ -31,10 +33,18 @@ class MainScreen(Screen): def __init__(self, *args, **kwargs): super().__init__(id="main_screen", *args, **kwargs) self.title = "Egeria" - self.sub_title = "My Profile" self.other_function_list: OptionList = OptionList(id="other_function_list") self.selected_table = None self.selected_row = None + load_app_config() + app_user = settings.User_Profile + self.user_name = app_user.user_name or "garygeeke" + self.sub_title = f"User: {self.user_name}" + + def on_mount(self) -> None: + # self.log needs a running app, so it can't be called from __init__ + self.log(f"Application Config: {settings.Environment}") + self.log(f"User Profile: {settings.User_Profile}") def compose(self) -> ComposeResult: # place widgets into grid on screen, note sequence determines position! @@ -54,7 +64,7 @@ def compose(self) -> ComposeResult: Static(f"Other Functions"), Static(f"[b]Select a function[/b]"), OptionList( - Option("[] User Identities"), + # Option("[] User Identities"), Option("[] Catalogs/Shop for Data"), Option("[] Edit Profile"), Option("[] Subscriptions"), @@ -68,7 +78,7 @@ def compose(self) -> ComposeResult: id="other_function_container" ) yield ScrollableContainer( - Static("Roles"), + Static("Roles [dim](Enter on a TeamLeader/TeamMember role: show team)[/dim]"), DataTable(id="roles_table"), id="main_roles_container" ) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MyBookMarksScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MyBookMarksScreen.py index 8d3a16f2..c29ea38d 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MyBookMarksScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MyBookMarksScreen.py @@ -20,9 +20,10 @@ class MyBookMarksScreen(ModalScreen): """ BINDINGS = [ - ("q", "quit", "Quit"), - ("ctrl+n", "new_bookmark", "Add New Bookmark"), - ("d", "remove_bookmark", "Remove Highlighted Bookmark"), + ("escape", "quit", "Back"), + ("q", "quit", "Main Menu"), + ("ctrl+n", "new_bookmark", "Add Bookmark"), + ("ctrl+d", "remove_bookmark", "Remove Bookmark"), ] CSS_PATH = "my_profile.tcss" @@ -39,7 +40,7 @@ def __init__(self, my_bookmarks, *args, target_guid: str | None = None, **kwargs def compose(self) -> ComposeResult: yield Header(show_clock=True) yield ScrollableContainer( - Static("Your bookmarks. Ctrl+N adds one by GUID; 'd' removes the highlighted one."), + Static("Your bookmarks. Ctrl+N adds one by GUID; Ctrl+D removes the highlighted one."), DataTable(id="my_bookmark_table", zebra_stripes=True, cursor_type="row"), id="bookmarks_table_container", ) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py index b2f42a50..cf6c02eb 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MyNetworkScreen.py @@ -64,9 +64,10 @@ class MyNetworkScreen(ModalScreen): """ BINDINGS = [ - ("q", "quit", "Quit"), - ("v", "view_details", "View Highlighted Person"), - ("d", "remove_peer", "Remove Highlighted Person"), + ("escape", "quit", "Back"), + ("q", "quit", "Main Menu"), + ("ctrl+o", "view_details", "Open Person"), + ("ctrl+d", "remove_peer", "Remove Person"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py index 6a01f8d4..8ac9674d 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/MyTeamScreen.py @@ -18,8 +18,8 @@ class MyTeam(ModalScreen): """ Display a list of team Members for a Team Leader """ BINDINGS = [ - ("q", "quit", "Quit"), - ("b", "back", "Go back") + ("escape", "back", "Back"), + ("q", "quit", "Main Menu"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/RETURN_CODES.md b/my_egeria/my_egeria/DemoCode/My_Profile/RETURN_CODES.md index de3e376e..080c92e5 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/RETURN_CODES.md +++ b/my_egeria/my_egeria/DemoCode/My_Profile/RETURN_CODES.md @@ -48,7 +48,7 @@ In accordance with application conventions: | **`423`** | `BUSINESS_DOMAIN_SPEC_ERROR` | Error executing BusinessCapabilities report specification in Shop for Data handler. | `shop_for_data_handler.py` | | **`429`** | `SAMPLE_DATA_RETRIEVAL_ERROR` | Failed to retrieve or parse sample data from Egeria Asset Catalog / Data Engine in Shop for Data handler. | `shop_for_data_handler.py` | | **`430`** | `INCOMPATIBLE_ENVIRONMENT` | App started under a Python whose pydantic is older than pyegeria requires, so request bodies would reach Egeria with their parameters silently missing. The app exits at startup with a message naming the interpreter and how to run from the project venv instead. | `my_profile_app.py`, `profile_utils.py` | -| **`440`** | `TEAM_ROLES_OR_GLOSSARY_ERROR` | Error communicating with Egeria or retrieving team members, user roles, or glossary term details. | `team_roles_handler.py` | +| **`440`** | `TEAM_ROLES_OR_GLOSSARY_ERROR` | Error communicating with Egeria or retrieving glossary term details. (A failed team members lookup is now shown as an error notification and does not exit the app.) | `team_roles_handler.py` | --- @@ -59,4 +59,4 @@ In accordance with application conventions: - **410–419**: Tree resolution errors in selection overview, and technology type data extraction / callback payload errors. - **420–429**: Data retrieval exceptions, report specification execution failures, and template instantiation failures across Shop for Data and Technology Types. - **430**: Incompatible Python environment detected at startup. -- **440**: Team roles and glossary term query communication errors. +- **440**: Glossary term query communication errors. diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py index 9404b0bc..af2ef78a 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/SearchForTermScreen.py @@ -17,8 +17,7 @@ class SearchForTermScreen(ModalScreen): - BINDINGS = [("q", "quit", "Quit"), - ("g", "go_back", "Go back"), + BINDINGS = [("escape", "go_back", "Back"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py index 41ee4a3f..1ef46122 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/SelectionOverviewScreen.py @@ -21,9 +21,9 @@ class SelectionOverviewScreen(ModalScreen): """Screen to display the selection of the user's data sources.""" - BINDINGS = [("q", "quit", "Quit"), - ("s", "subscribe", "Subscribe to Data Source"), - ("b", "back", "Go back")] + BINDINGS = [("escape", "back", "Back"), + ("q", "quit", "Main Menu"), + ("ctrl+u", "subscribe", "Subscribe")] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py index b99c2937..35cd4e08 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ShopForDataScreen.py @@ -11,7 +11,6 @@ from pyegeria import load_app_config, settings from textual import on from textual.app import ComposeResult -from textual.binding import Binding from textual.containers import ScrollableContainer from textual.events import Action from textual.screen import Screen @@ -20,14 +19,13 @@ class ShopForDataScreen(Screen): """ Screen to Present a choice of different data sources to the user.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("b", "back", "Go back"), - Binding("escape", "back", "Go back", show=False), - ("s", "sample_data_source", "Sample data source"), - ("u", "subscribe_to_data_source", "Subscribe"), - ("ctrl+s", "subscribe_to_data_source", "Subscribe"), - ("t", "search_for_term", "Search Glossary Terms"), - ("k", "bookmark_row", "Bookmark"), + BINDINGS = [("escape", "back", "Back"), + # Opened straight from the main screen, so back and main menu are the same place + ("q", "back", "Main Menu"), + ("ctrl+l", "sample_data_source", "Sample Data"), + ("ctrl+u", "subscribe_to_data_source", "Subscribe"), + ("ctrl+g", "search_for_term", "Search Glossary Terms"), + ("ctrl+k", "bookmark_row", "Bookmark Row"), ("ctrl+b", "bookmarks", "Bookmarks")] CSS_PATH = "my_profile.tcss" @@ -89,7 +87,7 @@ def compose(self) -> ComposeResult: """ Compose the UI components for the ShopForDataScreen screen.""" yield Header(show_clock=True) # A Static, not a TextArea: a focused TextArea swallows the screen's letter key bindings (b, q, s, ...) - yield Static("\nPlease click on a line to select a data source, or press b or Escape to go back:") + yield Static("\nPlease click on a line to select a data source, or press Escape to go back:") yield ScrollableContainer( Static("Glossary"), self.glossary_table) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py index c27df8d7..2a124655 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ShowCommentsScreen.py @@ -21,9 +21,10 @@ class ShowCommentsScreen(ModalScreen): """Show any comments for an element Screen for My Profile App.""" BINDINGS = [ - ("q", "quit", "Quit"), - ("ctrl+a", "add_comment", "Add Comment"), - ("ctrl+r", "add_response", "Add Response"), + ("escape", "quit", "Back"), + ("q", "quit", "Main Menu"), + ("ctrl+n", "add_comment", "Add Comment"), + ("ctrl+o", "add_response", "Open Responses"), ] CSS_PATH = "my_profile.tcss" @@ -40,7 +41,7 @@ def __init__( load_app_config() # The comment whose responses this screen shows (None = show an element's comments) self.selected_comment_guid = selected_comment_guid or None - # The comment currently highlighted in the DataTable (target for Ctrl+R) + # The comment currently highlighted in the DataTable (target for Ctrl+O) self.highlighted_comment_guid: str | None = None app_config = settings.Environment app_user = settings.User_Profile @@ -116,15 +117,15 @@ def load_comments(self, element_guid: str) -> None: if comment_table.row_count == 0: if self.selected_comment_guid: - self.notify("This comment has no responses yet - use Ctrl+A to add one", timeout=10) + self.notify("This comment has no responses yet - use Ctrl+N to add one", timeout=10) else: - self.notify("Selected element does not have any comments - use Ctrl+A to add one", timeout=10) + self.notify("Selected element does not have any comments - use Ctrl+N to add one", timeout=10) return # Mount the table once, after all rows have been added container = self.query_one("#show_comments_container", ScrollableContainer) container.mount(comment_table) - container.mount(Static("Ctrl+R on a highlighted comment to view and add responses to it")) + container.mount(Static("Ctrl+O on a highlighted comment to view and add responses to it")) def extract_backend_identifier(self, table_name, row_key): try: diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py index 90ca4695..f7c6a000 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/StatusScreen.py @@ -18,10 +18,11 @@ class StatusScreen(ModalScreen): """Modal screen to display the status of the application.""" - BINDINGS = [("q", "quit", "Quit"), - ("return", "successful", "Continue"), - ("b", "unsuccessful", "Bad result"), - ("c", "copy_guid_to_clipboard", "Copy GUID to clipboard"), + BINDINGS = [("enter", "successful", "Continue"), + ("escape", "successful", "Back"), + ("q", "quit", "Main Menu"), + ("ctrl+y", "copy_guid_to_clipboard", "Copy GUID"), + ("ctrl+x", "unsuccessful", "Bad Result"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py b/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py index fd5c4309..9a53c545 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/TechnologyTypeScreens.py @@ -21,9 +21,8 @@ class TechnologyTypeOptionsScreen(ModalScreen): """ Modal screen to display a technology type's templates and processes.""" - BINDINGS = [("q", "dismiss", "Quit"), - ("b", "go_back", "Go_Back"), - # ("ctl+s", "Select", "tech_type_option_select") + BINDINGS = [("escape", "go_back", "Back"), + ("q", "quit", "Main Menu"), ] CSS_PATH = "my_profile.tcss" @@ -239,7 +238,7 @@ def action_go_back(self) -> None: """ Handle the back key press.""" self.dismiss("back") - def action_Quit(self) -> None: + def action_quit(self) -> None: """ Handle the quit key press""" self.dismiss(200) @@ -261,8 +260,7 @@ def handle_process_selected(self, event: Button.Pressed) -> None: class TechnologyTypeProcessesScreen(ModalScreen[Any]): """Modal screen to display technology type processes in Egeria.""" - BINDINGS = [("q", "quit", "Quit"), - ("b", "quit", "Go back")] + BINDINGS = [("escape", "quit", "Back")] CSS_PATH = "my_profile.tcss" @@ -406,9 +404,9 @@ def handle_input_changed(self, event: Input.Changed) -> None: class TechnologyTypesScreen(ModalScreen): """Modal screen to display technology types in Egeria.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("g", "go_back", "Back"), - ("ctrl+e", "expand", "Toggle Twisties")] + BINDINGS = [("escape", "go_back", "Back"), + ("q", "quit", "Main Menu"), + ("ctrl+e", "expand", "Expand/Collapse All")] CSS_PATH = "my_profile.tcss" @@ -543,8 +541,7 @@ def render_tech_type_hierarchy_to_tree(self, class TechnologyTypeTemplatesScreen(ModalScreen[Any]): """Modal screen to display technology type templates in Egeria.""" - BINDINGS = [("q", "dismiss(200)", "Quit"), - ("b", "quit", "Go back")] + BINDINGS = [("escape", "quit", "Back")] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py index 4ff72ede..7888e651 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/UserIdentitiesScreen.py @@ -19,8 +19,8 @@ class UserIdentitiesScreen(ModalScreen): """ Modal Screen to display the user identities as stored in Egeria """ BINDINGS = [ - ("escape", "app.pop_screen", "Exit Screen"), - ("q", "app.pop_screen", "Exit Screen"), + ("escape", "app.pop_screen", "Back"), + ("q", "app.pop_screen", "Main Menu"), ] CSS_PATH = "my_profile.tcss" diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md b/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md index 4109a9c7..d84ec809 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md +++ b/my_egeria/my_egeria/DemoCode/My_Profile/VIDEO_SCRIPT_DRAFT.md @@ -49,17 +49,17 @@ > This panel is my working life: my **blog**, my **journal**, and my **to-dos**. > Down here is my **user identity**, which is how Egeria recognises me across the tools I use. -**[ON-SCREEN CALLOUT]** Point out the keyboard shortcuts in the footer: `Ctrl+T` Edit, `Ctrl+S` Comments, `Ctrl+B` Bookmarks. +**[ON-SCREEN CALLOUT]** Point out the keyboard shortcuts in the footer: `Ctrl+T` Edit, `Ctrl+O` Comments, `Ctrl+B` Bookmarks. --- ## 4. Keeping it current (1:45 – 2:20) -**[VISUAL]** Select the To-Dos table, press `Ctrl+T`, and add a to-do: *"Review clinical trial data quality report"*. Back on the main screen the new row appears. Then highlight a row and press `Ctrl+S` to show and add a comment. +**[VISUAL]** Select the To-Dos table, press `Ctrl+T`, and add a to-do: *"Review clinical trial data quality report"*. Back on the main screen the new row appears. Then highlight a row and press `Ctrl+O` to show and add a comment. **CALLIE:** > The profile is only useful if it's accurate. I pick any table and press **Ctrl+T** to edit it: add a row, update one, or remove something that's no longer true. Here I'm adding a to-do from this morning's stand-up. -> **Ctrl+S** opens the comments on whatever row I've selected, so a conversation stays attached to the thing it's about, not lost in email. +> **Ctrl+O** opens the comments on whatever row I've selected, so a conversation stays attached to the thing it's about, not lost in email. **[OPTIONAL]** Quick shot of **Edit Profile** from the Other Functions list, updating the job title. @@ -67,7 +67,7 @@ ## 5. Shopping for data (2:20 – 3:05) ← *the "hero" moment* -**[VISUAL]** Other Functions → **Catalogs / Shop for Data**. Show the Glossary, Digital Product Catalog, Data Dictionary, Business Domain, and Root Collection panels. Select a data product, press `s` to **sample** it, then `u` to **subscribe**. Fill in the subscription request and submit. Then open **Subscriptions** to show it listed. +**[VISUAL]** Other Functions → **Catalogs / Shop for Data**. Show the Glossary, Digital Product Catalog, Data Dictionary, Business Domain, and Root Collection panels. Select a data product, press `Ctrl+L` to **sample** it, then `Ctrl+U` to **subscribe**. Fill in the subscription request and submit. Then open **Subscriptions** to show it listed. **CALLIE:** > This is the part I use most. **Shop for Data** brings together our glossary, the digital product catalog, data dictionaries, and business domains in one place. @@ -110,7 +110,7 @@ - **Tone:** first person, warm, practical, a little wry. It's one colleague showing another, not a feature tour. Each feature should be tied to a job Callie actually needs to get done. - **Screen capture:** use a large terminal font (≥18pt) and a dark theme matching `my_profile.tcss`. Record at 1920×1080 and zoom in on the panel being discussed. Pause about half a second before each keypress so viewers can follow. - **Demo data:** record against the Coco Pharmaceuticals demo data, so the teams, roles, and data products shown match the rest of the series. Pre-create a few to-dos, blog and journal entries, and one subscribable data product. -- **Captions:** burn in the keyboard shortcuts (`Ctrl+T`, `Ctrl+S`, `Ctrl+B`, `s`, `u`) as they're used. +- **Captions:** burn in the keyboard shortcuts (`Ctrl+T`, `Ctrl+O`, `Ctrl+B`, `Ctrl+L`, `Ctrl+U`) as they're used. - **Before recording:** check every step against the current build, especially Change User, Create Profile, and Subscribe, since they've changed recently. - **LinkedIn post (draft):** > Meet My Profile 👋 A terminal app for Egeria users that shows your teams, roles, projects, and to-dos in one place, and lets you shop for governed data and subscribe to it in a few keystrokes. Watch Callie Quartile from Coco Pharmaceuticals take it for a spin: [YouTube link] #Egeria #OpenMetadata #DataGovernance #LFAIData diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/ViewSubscriptionsScreen.py b/my_egeria/my_egeria/DemoCode/My_Profile/ViewSubscriptionsScreen.py index 0b12122a..ed8784f1 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/ViewSubscriptionsScreen.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/ViewSubscriptionsScreen.py @@ -19,7 +19,8 @@ class ViewSubscriptionsScreen(ModalScreen): """View the digital subscriptions created by the current user.""" BINDINGS = [ - ("q", "quit", "Quit"), + ("escape", "quit", "Back"), + ("q", "quit", "Main Menu"), ] CSS_PATH = "my_profile.tcss" @@ -37,7 +38,7 @@ def __init__(self, *args, **kwargs): def compose(self) -> ComposeResult: yield Header(show_clock=True) yield ScrollableContainer( - Static("Your digital subscriptions. Press q to return.", id="view_subsciptions_static"), + Static("Your digital subscriptions. Press Escape to go back.", id="view_subsciptions_static"), DataTable(id="subscriptions_table", zebra_stripes=True, cursor_type="row"), id="view-subscriptions_container", ) diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py index 336e1dbf..281e6cb0 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/elements_crud_handler.py @@ -242,7 +242,7 @@ def show_comments(self, table_name: str | None = None, row_k: Any = None, """Show comments for the selected table row, or the responses to a selected comment. When selected_comment_guid is given, the screen shows the comments attached to that - comment (its responses) and Ctrl+A adds a new response to it. + comment (its responses) and Ctrl+N adds a new response to it. """ self.log(f"Showing comments for table: {table_name}, row: {row_k}, comment: {selected_comment_guid}") self.push_screen( diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py index 2b3674bf..79710c50 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/my_profile_app.py @@ -104,7 +104,7 @@ class MyProfileApp(App, TechTypesMixin, ShopForDataMixin, TeamRolesMixin, Elemen # means ctrl+f wins over the Input/TextArea "delete word right" binding on the same key. BINDINGS = [ ("q", "quit", "Quit"), - ("r", "refresh", "Refresh Data"), + ("ctrl+r", "refresh", "Refresh Data"), Binding("ctrl+f", "feedback", "Feedback", priority=True), Binding("f3", "view_feedback_log", "Feedback Log", priority=True), ] diff --git a/my_egeria/my_egeria/DemoCode/My_Profile/team_roles_handler.py b/my_egeria/my_egeria/DemoCode/My_Profile/team_roles_handler.py index 0b050795..e9d3fdc3 100644 --- a/my_egeria/my_egeria/DemoCode/My_Profile/team_roles_handler.py +++ b/my_egeria/my_egeria/DemoCode/My_Profile/team_roles_handler.py @@ -70,8 +70,17 @@ def handle_roles_table_row_selection(self, event: DataTable.RowSelected) -> Any: if not isinstance(result, (tuple, list)) or len(result) != 5: return team_members_list, team_display_name, team_qualified_name, team_category, team_description = result + if team_display_name is None: + # find_team_members has already told the user why there is no team to show + return else: self.log(f"Selected role is not a TeamLeader or TeamMember: {selected_role_name}") + self.notify( + f"'{selected_role_name}' is not a team role, so there are no team members to show. " + "Select a TeamLeader or TeamMember role.", + timeout=5, + severity="warning", + ) return 201 self.log(f"team_members_list: {team_members_list}") @@ -131,7 +140,7 @@ def find_team_members(self, role_name: str) -> list[Any]: except PyegeriaException as e: print_basic_exception(e) self.log(f"Error retrieving team members: {e!s}") - self.exit(440) + self.notify(f"Could not retrieve the team members from Egeria: {e!s}", timeout=20, severity="error") return [[], None, None, None, None] self.log(f"team_member_data: {team_member_data}") diff --git a/tests/micro-tests/my_profile/test_key_bindings.py b/tests/micro-tests/my_profile/test_key_bindings.py new file mode 100644 index 00000000..d71692e2 --- /dev/null +++ b/tests/micro-tests/my_profile/test_key_bindings.py @@ -0,0 +1,130 @@ +""" + PDX-License-Identifier: Apache-2.0 + Copyright Contributors to the ODPi Egeria project. + + Guards the app-wide key binding convention so every My Profile screen behaves the same: + escape back one level (every screen except Main) + q the only plain-letter key: Main Menu on sub-screens, Quit on Main. + Left off text-entry forms, where it would be typed into the Input instead. + ctrl+* every other action, one meaning per key (see docs/my_profile_app_manual.md) +""" + +import inspect + +import pytest +from textual.app import App +from textual.binding import Binding +from textual.screen import Screen +from textual.widgets import Input + +import AddCommentScreen +import AddToElementsScreens +import CreateProfileScreen +import CreateSubscriptionRequestScreen +import EditElementsScreens +import FeedbackScreens +import GenericDataViewScreen +import MainScreen +import MyBookMarksScreen +import MyNetworkScreen +import MyTeamScreen +import SearchForTermScreen +import SelectionOverviewScreen +import ShopForDataScreen +import ShowCommentsScreen +import SplashScreen +import StatusScreen +import TechnologyTypeScreens +import UserIdentitiesScreen +import ViewSubscriptionsScreen +from my_profile_app import MyProfileApp + +MODULES = [ + AddCommentScreen, AddToElementsScreens, CreateProfileScreen, CreateSubscriptionRequestScreen, + EditElementsScreens, FeedbackScreens, GenericDataViewScreen, MainScreen, MyBookMarksScreen, + MyNetworkScreen, MyTeamScreen, SearchForTermScreen, SelectionOverviewScreen, ShopForDataScreen, + ShowCommentsScreen, SplashScreen, StatusScreen, TechnologyTypeScreens, UserIdentitiesScreen, + ViewSubscriptionsScreen, +] + +SCREENS = sorted( + { + cls + for module in MODULES + for _, cls in inspect.getmembers(module, inspect.isclass) + if issubclass(cls, Screen) and cls.__module__ == module.__name__ + }, + key=lambda c: c.__name__, +) + +# ctrl+c is Textual's copy/quit, a terminal sends ctrl+i as Tab, ctrl+m/ctrl+j as Enter and +# ctrl+h as Backspace, and ctrl+p opens the command palette. "return" is not a Textual key name. +UNUSABLE_KEYS = {"ctrl+c", "ctrl+i", "ctrl+m", "ctrl+j", "ctrl+h", "ctrl+p", "return"} + + +def _bindings(cls): + for b in cls.BINDINGS: + key, action = (b.key, b.action) if isinstance(b, Binding) else (b[0], b[1]) + for k in key.split(","): + yield k.strip(), action + + +@pytest.mark.parametrize("screen_cls", SCREENS, ids=lambda c: c.__name__) +def test_only_q_is_a_plain_letter(screen_cls): + letters = [k for k, _ in _bindings(screen_cls) if len(k) == 1 and k != "q"] + assert not letters, f"{screen_cls.__name__} binds plain letter(s) {letters}; use a ctrl key" + + +@pytest.mark.parametrize("screen_cls", [s for s in SCREENS if s is not MainScreen.MainScreen], + ids=lambda c: c.__name__) +def test_escape_goes_back(screen_cls): + assert "escape" in dict(_bindings(screen_cls)), f"{screen_cls.__name__} has no escape binding" + + +@pytest.mark.parametrize("screen_cls", SCREENS + [MyProfileApp], ids=lambda c: c.__name__) +def test_no_unusable_keys(screen_cls): + bad = [k for k, _ in _bindings(screen_cls) if k in UNUSABLE_KEYS] + assert not bad, f"{screen_cls.__name__} binds {bad}, which never reach the app" + + +@pytest.mark.parametrize("screen_cls", SCREENS, ids=lambda c: c.__name__) +def test_every_binding_has_an_action(screen_cls): + for key, action in _bindings(screen_cls): + if action.startswith("app."): + continue + name = action.split("(")[0] + assert hasattr(screen_cls, f"action_{name}"), f"{screen_cls.__name__}: {key} -> missing action_{name}" + + +class _Host(App): + """Pushes one screen and records what it dismisses with.""" + + def __init__(self, screen): + super().__init__() + self._screen = screen + self.dismissed = "not dismissed" + + def on_mount(self): + self.push_screen(self._screen, callback=lambda r: setattr(self, "dismissed", r)) + + +@pytest.mark.parametrize("key, expected", [("ctrl+g", "community"), ("ctrl+l", "identity"), + ("ctrl+r", "role"), ("ctrl+t", "team"), ("escape", 200)]) +async def test_edit_profile_keys_work_while_typing(key, expected): + app = _Host(EditElementsScreens.EditProfileScreen(user_profile={"displayName": "Gary"}, user_GUID="g-1")) + async with app.run_test() as pilot: + await pilot.pause() + app.screen.query_one("#user_job_title", Input).focus() + await pilot.press(key) + await pilot.pause() + assert app.dismissed == expected + + +@pytest.mark.parametrize("key", ["enter", "escape", "q"]) +async def test_status_screen_close_keys(key): + app = _Host(StatusScreen.StatusScreen("Created element 'guid-1'")) + async with app.run_test() as pilot: + await pilot.pause() + await pilot.press(key) + await pilot.pause() + assert app.dismissed == 200 diff --git a/tests/micro-tests/my_profile/test_screens.py b/tests/micro-tests/my_profile/test_screens.py index b2994cd5..6caad2aa 100644 --- a/tests/micro-tests/my_profile/test_screens.py +++ b/tests/micro-tests/my_profile/test_screens.py @@ -764,7 +764,7 @@ async def test_shop_for_data_screen_bookmark_highlighted_row(self): target.focus() target.move_cursor(row=0) await pilot.pause() - await pilot.press("k") + await pilot.press("ctrl+k") await pilot.pause() app.bookmark_table_row.assert_called_once_with(target, row_key) @@ -776,7 +776,7 @@ async def test_shop_for_data_screen_default_config(self): await pilot.pause() assert app.dismissed_result == [210] - @pytest.mark.parametrize("key", ["b", "escape"]) + @pytest.mark.parametrize("key", ["escape", "q"]) @pytest.mark.asyncio async def test_shop_for_data_screen_go_back_key(self, key): # Pressed as soon as the screen opens, without focusing a table first: nothing on the diff --git a/tests/micro-tests/my_profile/test_team_roles_handler.py b/tests/micro-tests/my_profile/test_team_roles_handler.py index 35221cd5..04cfcb76 100644 --- a/tests/micro-tests/my_profile/test_team_roles_handler.py +++ b/tests/micro-tests/my_profile/test_team_roles_handler.py @@ -24,6 +24,7 @@ class DummyTeamRolesApp(TeamRolesMixin): def __init__(self, backend=None): self.pushed_screens = [] + self.notifications = [] self.exit_code = None self.shown_main_screen = False self.log_messages = [] @@ -49,6 +50,9 @@ def push_screen(self, screen, callback=None): def exit(self, code): self.exit_code = code + def notify(self, message, **kwargs): + self.notifications.append((message, kwargs.get("severity"))) + def status_callback(self, *args, **kwargs): pass @@ -76,6 +80,22 @@ def test_handle_roles_table_row_selection_non_team(self): res = app.handle_roles_table_row_selection(mock_event) assert res == 201 assert len(app.pushed_screens) == 0 + # The user is told why nothing opened + assert len(app.notifications) == 1 + message, severity = app.notifications[0] + assert "DataSteward" in message and "not a team role" in message + assert severity == "warning" + + def test_handle_roles_table_row_selection_failed_lookup_opens_no_team_screen(self, backend): + # Always faked: an 'empty' result. Only the StatusScreen explaining it is shown, not MyTeam too. + app = DummyTeamRolesApp(backend) + backend.always_fake("team_roles_handler.exec_report_spec", returns={"kind": "empty"}) + mock_event = MagicMock() + mock_event.data_table.get_row.return_value = ["TeamLeader::Department::101", "PersonRole", "Desc", "g1"] + + app.handle_roles_table_row_selection(mock_event) + + assert [type(screen) for screen, _ in app.pushed_screens] == [StatusScreen] @pytest.mark.live_capable def test_handle_roles_table_row_selection_team_leader( @@ -138,8 +158,12 @@ def test_find_team_members_pyegeria_exception(self, backend): backend.always_fake("team_roles_handler.exec_report_spec", side_effect=PyegeriaException("API Error")) members, dname, qname, cat, desc = app.find_team_members("Department::101::TeamLeader") - assert members == [] - assert app.exit_code == 440 + assert members == [] and dname is None + # Reported to the user; the app keeps running + assert app.exit_code is None + assert len(app.notifications) == 1 + message, severity = app.notifications[0] + assert "API Error" in message and severity == "error" def test_find_team_members_empty_kind(self, backend): # Always faked: an 'empty' result is not something a live server can be asked for.